diff --git a/surreal-poll/.env.example b/surreal-poll/.env.example new file mode 100644 index 0000000..004ab59 --- /dev/null +++ b/surreal-poll/.env.example @@ -0,0 +1,11 @@ +# expose client-side environment variables. +# this environment variables are exposed in your bundle. + +VITE_DB_HOST= +VITE_DB_USER= +VITE_DB_PASS= + +# This token is for when you want to use the Surreal Cloud +# if you want to import the schama's that are already created +# inside the schema folder inside this project. +DB_TOKEN= \ No newline at end of file diff --git a/surreal-poll/.gitignore b/surreal-poll/.gitignore new file mode 100644 index 0000000..a0d218e --- /dev/null +++ b/surreal-poll/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env \ No newline at end of file diff --git a/surreal-poll/Makefile b/surreal-poll/Makefile new file mode 100644 index 0000000..6450b82 --- /dev/null +++ b/surreal-poll/Makefile @@ -0,0 +1,12 @@ +# Include .env file +-include .env +# Make sure variables are exported +export + +apply: + @echo "Applying schema" + $(eval MODIFIED_DB_HOST := $(subst wss://,https://,${VITE_DB_HOST})) + for schema in $(wildcard schema/*.surql); do \ + echo "Applying schema: $$schema"; \ + surreal import $$schema --namespace surrealdb --database pollwebapp --endpoint ${MODIFIED_DB_HOST} --token ${DB_TOKEN};\ + done \ No newline at end of file diff --git a/surreal-poll/README.md b/surreal-poll/README.md new file mode 100644 index 0000000..6331912 --- /dev/null +++ b/surreal-poll/README.md @@ -0,0 +1,143 @@ +# SurrealPolls 🗳️– Your Gateway to Instant Opinions! + +Welcome to **SurrealPolls**, where gathering votes and making decisions has never been easier (or more fun)! Whether you’re planning a group outing, running a class survey, or settling the eternal "pineapple on pizza" debate, we’ve got you covered. + +Built with cutting-edge technologies like **SolidJS**, **Vite**, **SurrealDB**, **Surreal Cloud**, and **Tailwind CSS**, this app is as modern as your need for instant polls. + +--- + +## What’s Inside the Polling Magic? + +### **Tech Stack Highlights**: +- **SolidJS**: Super snappy frontend framework for blazing-fast performance and reactivity. Your votes will feel instant, and your app will feel buttery smooth. +- **Tailwind CSS**: Your app is not just functional; it’s also pretty, thanks to this utility-first CSS framework. +- **Surreal Cloud**: Surreal Cloud (Beta) transforms the database experience, providing the power and versatility of SurrealDB without the complexity of managing infrastructure. + + + +--- + +## Features That’ll Make You Smile 😄 + +### 📝 **Create Polls** +Spin up a poll in seconds. Just give it a title, list out some options, and BOOM! A shareable link is ready for your friends, colleagues, or anyone with an opinion. + +### ✅ **Vote Away** +Let your participants cast their votes effortlessly. The app ensures a seamless and real-time voting experience, so nobody’s left waiting. + +### 🔒 **End Polls** +Want to close the poll and announce the results? No problem! You’re in control. End the poll whenever you’re ready to seal the deal. + +### 📊 **Real-Time Results** +Results update in real-time, giving you instant insights into where everyone stands. No refresh button required! + +--- + +## How to Run This Beauty + +### Prerequisites +Make sure you have the following installed: +- [Node.js](https://nodejs.org) (v16+) +- [SurrealDB](https://surrealdb.com) (running locally or remotely) + +### Installation +Ready to dive in? Let’s go! + +1. Clone the repo: + ```bash + git clone https://github.com/your-repo/polling-app.git + cd polling-app + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Connect to a SurrealDB Cloud Instance via CLI: + + When you have a SurrealDB Cloud instance running, you can [connect to it using the SurrealDB CLI](https://surrealdb.com/docs/cloud/connect/cli) and this will give you an endpoint to use in the make file. + + Copy the endpoint without the `surreal sql` prefix as you will be making an import into the database. + + ```bash + --endpoint --token + ``` + > We recommend using [Surrealist](https://surrealist.dev) the GUI for SurrealDB Cloud to manage and create your database users. + +4. Create a database user in the Authentication section of Surrealist as you will need to use the username and password to connect to the database from the frontend. + +4. Mirroring the `.env.example` file, create an `.env` file in the root of the project and set the `VITE_DB_HOST` and `DB_TOKEN` to the endpoint and token you copied from the CLI. Then, set the `VITE_DB_USER` to `root` and `VITE_DB_PASS` to `root`. + + +5. Apply the schema: Use the included Makefile to set up your database schema: + ```bash + make apply + ``` + This will automatically apply all .surql files in the schema directory to your SurrealDB instance. + +5. Configure SurrealDB in `src/lib/providers/surrealdb.tsx` if needed (default is localhost). + +6. Run the development server: + ```bash + npm run dev + ``` + +7. Open your browser at [http://localhost:3000](http://localhost:3000) and start polling! + +--- + +## File Structure +Here’s how this masterpiece is organized: + +``` +/surrealpolls +|-- /src +| |-- /assets # Static assets like icons +| |-- /components # Reusable UI components +| | |-- Footer # Footer component +| | |-- Navbar # Navbar component +| |-- /lib # Logic and utility files +| | |-- /providers # Context providers (e.g., surrealdb.tsx) +| | |-- utils.ts # Helper functions +| |-- /pages # Pages for routing (Home, Create Poll, Vote, Results) +| |-- /styles # Global styles (index.css) +| |-- App.tsx # The main app component +| |-- index.tsx # Entry point of the app +| |-- routes.tsx # App routes +|-- /public # Public files (e.g., favicon) +|-- /.env.example # Example env file +|-- Makefile # For schema management +|-- tailwind.config.js # Tailwind configuration +|-- vite.config.ts # Vite configuration +``` + +--- + +## Things You Can Try +- **Create Your First Poll**: Go to the “Create Poll” page, add some options, and share the generated link. +- **Test Real-Time Voting**: Open the voting link in two different tabs/devices and watch the magic happen. +- **End a Poll**: Close a poll to stop further voting and see the results instantly. + +--- + +## Why Use This App? +- **Speedy Setup**: It’s quick to build and deploy—thanks to SolidJS and Vite. +- **Beautiful UI**: Tailwind CSS makes everything look polished without extra effort. +- **Powerful Backend**: SurrealDB ensures your data is safe, scalable, and lightning-fast. +- **Open Source**: Customize it to your heart’s content. + +--- + +## Contributing +Feel free to fork the repo, make improvements, and send us a pull request. We’re always open to ideas that make polling better (and cooler)! + +--- + +## License +This project is licensed under the MIT License—because sharing is caring. 😊 + +--- + +Happy polling! 🎉 + diff --git a/surreal-poll/bun.lockb b/surreal-poll/bun.lockb new file mode 100644 index 0000000..b8dac12 Binary files /dev/null and b/surreal-poll/bun.lockb differ diff --git a/surreal-poll/index.html b/surreal-poll/index.html new file mode 100644 index 0000000..7a5ca04 --- /dev/null +++ b/surreal-poll/index.html @@ -0,0 +1,18 @@ + + + + + + + + + SurrealDB | Poll webapp + + + + +
+ + + + \ No newline at end of file diff --git a/surreal-poll/package.json b/surreal-poll/package.json new file mode 100644 index 0000000..d3cc5a3 --- /dev/null +++ b/surreal-poll/package.json @@ -0,0 +1,35 @@ +{ + "name": "vite-template-solid", + "version": "0.0.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview" + }, + "license": "MIT", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.10.5", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^6.0.0", + "vite-plugin-solid": "^2.11.0" + }, + "dependencies": { + "@kobalte/core": "^0.13.7", + "@solidjs/router": "^0.15.2", + "@tanstack/solid-query": "^5.62.16", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "solid-js": "^1.9.3", + "surrealdb": "1.1.0", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7" + }, + "module": "index.ts" +} \ No newline at end of file diff --git a/surreal-poll/postcss.config.js b/surreal-poll/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/surreal-poll/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/surreal-poll/schema/poll.surql b/surreal-poll/schema/poll.surql new file mode 100644 index 0000000..8c49944 --- /dev/null +++ b/surreal-poll/schema/poll.surql @@ -0,0 +1,8 @@ +DEFINE TABLE poll SCHEMAFULL PERMISSIONS FULL; + +-- Title of the poll +DEFINE FIELD title ON poll TYPE string; + +-- The creator of the poll (if any) +DEFINE FIELD creator ON poll TYPE option> + VALUE $auth.id; \ No newline at end of file diff --git a/surreal-poll/schema/pollHasQuestion.surql b/surreal-poll/schema/pollHasQuestion.surql new file mode 100644 index 0000000..2ebd61d --- /dev/null +++ b/surreal-poll/schema/pollHasQuestion.surql @@ -0,0 +1,4 @@ +DEFINE TABLE pollHasQuestion SCHEMAFULL + TYPE RELATION + IN poll + OUT question; \ No newline at end of file diff --git a/surreal-poll/schema/pollQuestion.surql b/surreal-poll/schema/pollQuestion.surql new file mode 100644 index 0000000..5c71120 --- /dev/null +++ b/surreal-poll/schema/pollQuestion.surql @@ -0,0 +1,3 @@ +DEFINE TABLE pollQuestion SCHEMAFULL; + +DEFINE FIELD question ON pollQuestion TYPE string; \ No newline at end of file diff --git a/surreal-poll/schema/pollVote.surql b/surreal-poll/schema/pollVote.surql new file mode 100644 index 0000000..b832270 --- /dev/null +++ b/surreal-poll/schema/pollVote.surql @@ -0,0 +1,5 @@ +DEFINE TABLE pollVote SCHEMAFULL + TYPE RELATION + IN user + OUT pollQuestion; + diff --git a/surreal-poll/schema/user.surql b/surreal-poll/schema/user.surql new file mode 100644 index 0000000..1fe18c9 --- /dev/null +++ b/surreal-poll/schema/user.surql @@ -0,0 +1,19 @@ +DEFINE ACCESS user ON DATABASE TYPE RECORD + SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($pass) ) + SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $pass) ) + DURATION FOR TOKEN 15m, FOR SESSION 12h +; + +DEFINE TABLE user SCHEMAFULL + PERMISSIONS FOR select, update, delete + WHERE $access = "user" + AND id = $auth.id +; + +-- Give the user table an email field. Store it in a string +DEFINE FIELD email ON TABLE user + TYPE string + ASSERT string::is::email($value); + +DEFINE FIELD name ON TABLE user TYPE string; +DEFINE FIELD password ON TABLE user TYPE string; \ No newline at end of file diff --git a/surreal-poll/src/App.tsx b/surreal-poll/src/App.tsx new file mode 100644 index 0000000..b249753 --- /dev/null +++ b/surreal-poll/src/App.tsx @@ -0,0 +1,46 @@ +import type { Component } from 'solid-js'; +import { Router, Route } from "@solidjs/router"; +import { SurrealProvider } from './lib/providers/surrealdb'; +import { QueryClientProvider } from '@tanstack/solid-query'; +import { tanstackClient } from './lib/query-client'; +import { Navbar } from './components/Navbar'; +import { AuthProvider } from './lib/providers/auth'; +import Home from './pages/Home'; +import CreatePoll from './pages/CreatePoll'; +import VotePoll from './pages/VotePoll'; +import PollResults from './pages/PollResults'; +import { AppLayout } from './components/layout/app'; +import { Signin } from './pages/Signin'; +import { Signup } from './pages/Signup'; +import { Toaster } from './components/ui/toast'; + +const App: Component = () => { + return ( + + + + + + + + + + + + + + + + + + ); +}; + +export default App; diff --git a/surreal-poll/src/assets/favicon.ico b/surreal-poll/src/assets/favicon.ico new file mode 100644 index 0000000..b836b2b Binary files /dev/null and b/surreal-poll/src/assets/favicon.ico differ diff --git a/surreal-poll/src/components/Footer/index.tsx b/surreal-poll/src/components/Footer/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/surreal-poll/src/components/Navbar/avatar.tsx b/surreal-poll/src/components/Navbar/avatar.tsx new file mode 100644 index 0000000..cb69611 --- /dev/null +++ b/surreal-poll/src/components/Navbar/avatar.tsx @@ -0,0 +1,40 @@ +import { Avatar, AvatarFallback } from "../ui/avatar"; +import { useAuth } from "~/lib/providers/auth"; +import { For, Show } from "solid-js"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuShortcut, DropdownMenuTrigger } from "../ui/dropdown-menu"; +import { A } from "@solidjs/router"; + +export function NavbarAvatar() { + + // const { client } = useSurreal(); + const { user, logout } = useAuth(); + + return ( + + + + + {(user()?.name ?? "U").slice(0, 1).toUpperCase()} + + + + + + Logout + + } + > + + Signin + + + Signup + + + + + ); +} \ No newline at end of file diff --git a/surreal-poll/src/components/Navbar/index.tsx b/surreal-poll/src/components/Navbar/index.tsx new file mode 100644 index 0000000..f71b63d --- /dev/null +++ b/surreal-poll/src/components/Navbar/index.tsx @@ -0,0 +1,20 @@ +import { useSurreal } from "~/lib/providers/surrealdb"; +import { NavbarAvatar } from "./avatar"; +import { A } from "@solidjs/router"; + +export function Navbar() { + + const { } = useSurreal(); + + return ( +
+ + Home + + + Create Poll + + +
+ ); +} \ No newline at end of file diff --git a/surreal-poll/src/components/Vote/index.tsx b/surreal-poll/src/components/Vote/index.tsx new file mode 100644 index 0000000..28951d4 --- /dev/null +++ b/surreal-poll/src/components/Vote/index.tsx @@ -0,0 +1,15 @@ +import { PollQuestion } from "~/types/pollQuestion"; + +interface PollQuestionRowProps { + question: PollQuestion; + onVote: (question: PollQuestion) => void; +} + +export function PollQuestionRow(props: PollQuestionRowProps) { + + return ( +
props.onVote(props.question)} class="p-4 border border-gray-200 rounded-md cursor-pointer hover:bg-gray-100"> +

{props.question.question}

+
+ ); +} \ No newline at end of file diff --git a/surreal-poll/src/components/layout/app.tsx b/surreal-poll/src/components/layout/app.tsx new file mode 100644 index 0000000..6d95288 --- /dev/null +++ b/surreal-poll/src/components/layout/app.tsx @@ -0,0 +1,18 @@ +import { RouteSectionProps } from "@solidjs/router"; +import { Navbar } from "../Navbar"; +import { useAuth } from "~/lib/providers/auth"; + +export function AppLayout(props: RouteSectionProps) { + + const { user } = useAuth(); + + return ( + <> + +
+ {props.children} +
+ {JSON.stringify(user() ?? '{ user: "not" }')} + + ); +} \ No newline at end of file diff --git a/surreal-poll/src/components/ui/avatar.tsx b/surreal-poll/src/components/ui/avatar.tsx new file mode 100644 index 0000000..4fac599 --- /dev/null +++ b/surreal-poll/src/components/ui/avatar.tsx @@ -0,0 +1,51 @@ +import type { ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as ImagePrimitive from "@kobalte/core/image" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +type AvatarRootProps = ImagePrimitive.ImageRootProps & { + class?: string | undefined +} + +const Avatar = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarRootProps, ["class"]) + return ( + + ) +} + +type AvatarImageProps = ImagePrimitive.ImageImgProps & { + class?: string | undefined +} + +const AvatarImage = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarImageProps, ["class"]) + return +} + +type AvatarFallbackProps = + ImagePrimitive.ImageFallbackProps & { class?: string | undefined } + +const AvatarFallback = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as AvatarFallbackProps, ["class"]) + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/surreal-poll/src/components/ui/badge.tsx b/surreal-poll/src/components/ui/badge.tsx new file mode 100644 index 0000000..516fbf1 --- /dev/null +++ b/surreal-poll/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import type { Component, ComponentProps } from "solid-js" +import { splitProps } from "solid-js" + +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +import { cn } from "~/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + outline: "text-foreground", + success: "border-success-foreground bg-success text-success-foreground", + warning: "border-warning-foreground bg-warning text-warning-foreground", + error: "border-error-foreground bg-error text-error-foreground" + } + }, + defaultVariants: { + variant: "default" + } + } +) + +type BadgeProps = ComponentProps<"div"> & + VariantProps & { + round?: boolean + } + +const Badge: Component = (props) => { + const [local, others] = splitProps(props, ["class", "variant", "round"]) + return ( +
+ ) +} + +export type { BadgeProps } +export { Badge, badgeVariants } diff --git a/surreal-poll/src/components/ui/breadcrumb.tsx b/surreal-poll/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..0b9e2f1 --- /dev/null +++ b/surreal-poll/src/components/ui/breadcrumb.tsx @@ -0,0 +1,111 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { Show, splitProps } from "solid-js" + +import type { PolymorphicProps } from "@kobalte/core" +import * as BreadcrumbPrimitive from "@kobalte/core/breadcrumbs" + +import { cn } from "~/lib/utils" + +const Breadcrumb = BreadcrumbPrimitive.Root + +const BreadcrumbList: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( +
    + ) +} + +const BreadcrumbItem: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return
  1. +} + +type BreadcrumbLinkProps = + BreadcrumbPrimitive.BreadcrumbsLinkProps & { class?: string | undefined } + +const BreadcrumbLink = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as BreadcrumbLinkProps, ["class"]) + return ( + + ) +} + +type BreadcrumbSeparatorProps = + BreadcrumbPrimitive.BreadcrumbsSeparatorProps & { + class?: string | undefined + children?: JSX.Element + } + +const BreadcrumbSeparator = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as BreadcrumbSeparatorProps, ["class", "children"]) + return ( + svg]:size-3.5", local.class)} {...others}> + + + + } + > + {local.children} + + + ) +} + +const BreadcrumbEllipsis: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( + + + + + + + More + + ) +} + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbSeparator, + BreadcrumbEllipsis +} diff --git a/surreal-poll/src/components/ui/button.tsx b/surreal-poll/src/components/ui/button.tsx new file mode 100644 index 0000000..be27aeb --- /dev/null +++ b/surreal-poll/src/components/ui/button.tsx @@ -0,0 +1,53 @@ +import type { JSX, ValidComponent } from "solid-js"; +import { splitProps } from "solid-js"; + +import * as ButtonPrimitive from "@kobalte/core/button"; +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; + +import { cn } from "~/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline" + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 px-3 text-xs", + lg: "h-11 px-8", + icon: "size-10" + } + }, + defaultVariants: { + variant: "default", + size: "default" + } + } +); + +type ButtonProps = ButtonPrimitive.ButtonRootProps & + VariantProps & { class?: string | undefined; children?: JSX.Element; }; + +const Button = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"]); + return ( + + ); +}; + +export type { ButtonProps }; +export { Button, buttonVariants }; diff --git a/surreal-poll/src/components/ui/card.tsx b/surreal-poll/src/components/ui/card.tsx new file mode 100644 index 0000000..939806b --- /dev/null +++ b/surreal-poll/src/components/ui/card.tsx @@ -0,0 +1,43 @@ +import type { Component, ComponentProps } from "solid-js" +import { splitProps } from "solid-js" + +import { cn } from "~/lib/utils" + +const Card: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( +
    + ) +} + +const CardHeader: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return
    +} + +const CardTitle: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return ( +

    + ) +} + +const CardDescription: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return

    +} + +const CardContent: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return

    +} + +const CardFooter: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]) + return
    +} + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/surreal-poll/src/components/ui/checkbox.tsx b/surreal-poll/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..be2d636 --- /dev/null +++ b/surreal-poll/src/components/ui/checkbox.tsx @@ -0,0 +1,60 @@ +import type { ValidComponent } from "solid-js" +import { Match, splitProps, Switch } from "solid-js" + +import * as CheckboxPrimitive from "@kobalte/core/checkbox" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +type CheckboxRootProps = + CheckboxPrimitive.CheckboxRootProps & { class?: string | undefined } + +const Checkbox = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as CheckboxRootProps, ["class"]) + return ( + + + + + + + + + + + + + + + + + + + + ) +} + +export { Checkbox } diff --git a/surreal-poll/src/components/ui/dialog.tsx b/surreal-poll/src/components/ui/dialog.tsx new file mode 100644 index 0000000..96981fc --- /dev/null +++ b/surreal-poll/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as DialogPrimitive from "@kobalte/core/dialog" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +const Dialog = DialogPrimitive.Root +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal: Component = (props) => { + const [, rest] = splitProps(props, ["children"]) + return ( + +
    + {props.children} +
    +
    + ) +} + +type DialogOverlayProps = + DialogPrimitive.DialogOverlayProps & { class?: string | undefined } + +const DialogOverlay = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogOverlayProps, ["class"]) + return ( + + ) +} + +type DialogContentProps = + DialogPrimitive.DialogContentProps & { + class?: string | undefined + children?: JSX.Element + } + +const DialogContent = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogContentProps, ["class", "children"]) + return ( + + + + {props.children} + + + + + + Close + + + + ) +} + +const DialogHeader: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return ( +
    + ) +} + +const DialogFooter: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return ( +
    + ) +} + +type DialogTitleProps = DialogPrimitive.DialogTitleProps & { + class?: string | undefined +} + +const DialogTitle = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogTitleProps, ["class"]) + return ( + + ) +} + +type DialogDescriptionProps = + DialogPrimitive.DialogDescriptionProps & { + class?: string | undefined + } + +const DialogDescription = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DialogDescriptionProps, ["class"]) + return ( + + ) +} + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription +} diff --git a/surreal-poll/src/components/ui/dropdown-menu.tsx b/surreal-poll/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..9ff7a0a --- /dev/null +++ b/surreal-poll/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,260 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import * as DropdownMenuPrimitive from "@kobalte/core/dropdown-menu" +import type { PolymorphicProps } from "@kobalte/core/polymorphic" + +import { cn } from "~/lib/utils" + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger +const DropdownMenuPortal = DropdownMenuPrimitive.Portal +const DropdownMenuSub = DropdownMenuPrimitive.Sub +const DropdownMenuGroup = DropdownMenuPrimitive.Group +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenu: Component = (props) => { + return +} + +type DropdownMenuContentProps = + DropdownMenuPrimitive.DropdownMenuContentProps & { + class?: string | undefined + } + +const DropdownMenuContent = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuContentProps, ["class"]) + return ( + + + + ) +} + +type DropdownMenuItemProps = + DropdownMenuPrimitive.DropdownMenuItemProps & { + class?: string | undefined + } + +const DropdownMenuItem = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuItemProps, ["class"]) + return ( + + ) +} + +const DropdownMenuShortcut: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return +} + +const DropdownMenuLabel: Component & { inset?: boolean }> = (props) => { + const [, rest] = splitProps(props, ["class", "inset"]) + return ( +
    + ) +} + +type DropdownMenuSeparatorProps = + DropdownMenuPrimitive.DropdownMenuSeparatorProps & { + class?: string | undefined + } + +const DropdownMenuSeparator = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuSeparatorProps, ["class"]) + return ( + + ) +} + +type DropdownMenuSubTriggerProps = + DropdownMenuPrimitive.DropdownMenuSubTriggerProps & { + class?: string | undefined + children?: JSX.Element + } + +const DropdownMenuSubTrigger = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuSubTriggerProps, ["class", "children"]) + return ( + + {props.children} + + + + + ) +} + +type DropdownMenuSubContentProps = + DropdownMenuPrimitive.DropdownMenuSubContentProps & { + class?: string | undefined + } + +const DropdownMenuSubContent = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuSubContentProps, ["class"]) + return ( + + ) +} + +type DropdownMenuCheckboxItemProps = + DropdownMenuPrimitive.DropdownMenuCheckboxItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const DropdownMenuCheckboxItem = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuCheckboxItemProps, ["class", "children"]) + return ( + + + + + + + + + {props.children} + + ) +} + +type DropdownMenuGroupLabelProps = + DropdownMenuPrimitive.DropdownMenuGroupLabelProps & { + class?: string | undefined + } + +const DropdownMenuGroupLabel = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuGroupLabelProps, ["class"]) + return ( + + ) +} + +type DropdownMenuRadioItemProps = + DropdownMenuPrimitive.DropdownMenuRadioItemProps & { + class?: string | undefined + children?: JSX.Element + } + +const DropdownMenuRadioItem = ( + props: PolymorphicProps> +) => { + const [, rest] = splitProps(props as DropdownMenuRadioItemProps, ["class", "children"]) + return ( + + + + + + + + + {props.children} + + ) +} + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuPortal, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuShortcut, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuCheckboxItem, + DropdownMenuGroup, + DropdownMenuGroupLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem +} diff --git a/surreal-poll/src/components/ui/select.tsx b/surreal-poll/src/components/ui/select.tsx new file mode 100644 index 0000000..a938c34 --- /dev/null +++ b/surreal-poll/src/components/ui/select.tsx @@ -0,0 +1,109 @@ +import type { JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" + +import type { PolymorphicProps } from "@kobalte/core/polymorphic" +import * as SelectPrimitive from "@kobalte/core/select" + +import { cn } from "~/lib/utils" + +const Select = SelectPrimitive.Root +const SelectValue = SelectPrimitive.Value +const SelectHiddenSelect = SelectPrimitive.HiddenSelect + +type SelectTriggerProps = + SelectPrimitive.SelectTriggerProps & { + class?: string | undefined + children?: JSX.Element + } + +const SelectTrigger = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectTriggerProps, ["class", "children"]) + return ( + + {local.children} + + + + + + ) +} + +type SelectContentProps = + SelectPrimitive.SelectContentProps & { class?: string | undefined } + +const SelectContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectContentProps, ["class"]) + return ( + + + + + + ) +} + +type SelectItemProps = SelectPrimitive.SelectItemProps & { + class?: string | undefined + children?: JSX.Element +} + +const SelectItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectItemProps, ["class", "children"]) + return ( + + + + + + + + {local.children} + + ) +} + +export { Select, SelectValue, SelectHiddenSelect, SelectTrigger, SelectContent, SelectItem } diff --git a/surreal-poll/src/components/ui/separator.tsx b/surreal-poll/src/components/ui/separator.tsx new file mode 100644 index 0000000..5707717 --- /dev/null +++ b/surreal-poll/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import type { ValidComponent } from "solid-js"; +import { splitProps } from "solid-js"; + +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; +import * as SeparatorPrimitive from "@kobalte/core/separator"; + +import { cn } from "~/lib/utils"; + +type SeparatorRootProps = + SeparatorPrimitive.SeparatorRootProps & { class?: string | undefined; }; + +const Separator = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SeparatorRootProps, ["class", "orientation"]); + return ( + + ); +}; + +export { Separator }; diff --git a/surreal-poll/src/components/ui/sheet.tsx b/surreal-poll/src/components/ui/sheet.tsx new file mode 100644 index 0000000..89580cf --- /dev/null +++ b/surreal-poll/src/components/ui/sheet.tsx @@ -0,0 +1,172 @@ +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js"; +import { splitProps } from "solid-js"; + +import * as SheetPrimitive from "@kobalte/core/dialog"; +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "~/lib/utils"; + +const Sheet = SheetPrimitive.Root; +const SheetTrigger = SheetPrimitive.Trigger; +const SheetClose = SheetPrimitive.CloseButton; + +const portalVariants = cva("fixed inset-0 z-50 flex", { + variants: { + position: { + top: "items-start", + bottom: "items-end", + left: "justify-start", + right: "justify-end" + } + }, + defaultVariants: { position: "right" } +}); + +type PortalProps = SheetPrimitive.DialogPortalProps & VariantProps; + +const SheetPortal: Component = (props) => { + const [local, others] = splitProps(props, ["position", "children"]); + return ( + +
    {local.children}
    +
    + ); +}; + +type DialogOverlayProps = SheetPrimitive.DialogOverlayProps & { + class?: string | undefined; +}; + +const SheetOverlay = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as DialogOverlayProps, ["class"]); + return ( + + ); +}; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[closed=]:duration-300 data-[expanded=]:duration-500 data-[expanded=]:animate-in data-[closed=]:animate-out", + { + variants: { + position: { + top: "inset-x-0 top-0 border-b data-[closed=]:slide-out-to-top data-[expanded=]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[closed=]:slide-out-to-bottom data-[expanded=]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[closed=]:slide-out-to-left data-[expanded]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[closed=]:slide-out-to-right data-[expanded=]:slide-in-from-right sm:max-w-sm" + } + }, + defaultVariants: { + position: "right" + } + } +); + +type DialogContentProps = SheetPrimitive.DialogContentProps & + VariantProps & { class?: string | undefined; children?: JSX.Element; }; + +const SheetContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as DialogContentProps, ["position", "class", "children"]); + return ( + + + + {local.children} + + + + + + Close + + + + ); +}; + +const SheetHeader: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return ( +
    + ); +}; + +const SheetFooter: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return ( +
    + ); +}; + +type DialogTitleProps = SheetPrimitive.DialogTitleProps & { + class?: string | undefined; +}; + +const SheetTitle = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as DialogTitleProps, ["class"]); + return ( + + ); +}; + +type DialogDescriptionProps = + SheetPrimitive.DialogDescriptionProps & { class?: string | undefined; }; + +const SheetDescription = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as DialogDescriptionProps, ["class"]); + return ( + + ); +}; + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription +}; diff --git a/surreal-poll/src/components/ui/sidebar.tsx b/surreal-poll/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..4647bec --- /dev/null +++ b/surreal-poll/src/components/ui/sidebar.tsx @@ -0,0 +1,691 @@ +import type { Accessor, Component, ComponentProps, JSX, ValidComponent } from "solid-js"; +import { + createContext, + createEffect, + createMemo, + createSignal, + Match, + mergeProps, + onCleanup, + Show, + splitProps, + Switch, + useContext +} from "solid-js"; + +import type { PolymorphicProps } from "@kobalte/core"; +import { Polymorphic } from "@kobalte/core"; +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; + +import { cn } from "~/lib/utils"; +import type { ButtonProps } from "~/components/ui/button"; +import { Button } from "~/components/ui/button"; +import { Separator } from "~/components/ui/separator"; +import { Sheet, SheetContent } from "~/components/ui/sheet"; +import { Skeleton } from "~/components/ui/skeleton"; +import { TextField, TextFieldInput } from "~/components/ui/text-field"; +import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip"; + +const MOBILE_BREAKPOINT = 768; +const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContext = { + state: Accessor<"expanded" | "collapsed">; + open: Accessor; + setOpen: (open: boolean) => void; + openMobile: Accessor; + setOpenMobile: (open: boolean) => void; + isMobile: Accessor; + toggleSidebar: () => void; +}; + +const SidebarContext = createContext(null); + +function useSidebar() { + const context = useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a Sidebar."); + } + + return context; +} + +export function useIsMobile(fallback = false) { + const [isMobile, setIsMobile] = createSignal(fallback); + + createEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener("change", onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + onCleanup(() => mql.removeEventListener("change", onChange)); + }); + + return isMobile; +} + +type SidebarProviderProps = Omit, "style"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + style?: JSX.CSSProperties; +}; + +const SidebarProvider: Component = (rawProps) => { + const props = mergeProps({ defaultOpen: true }, rawProps); + const [local, others] = splitProps(props, [ + "defaultOpen", + "open", + "onOpenChange", + "class", + "style", + "children" + ]); + + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = createSignal(false); + + // This is the internal state of the sidebar. + // We use open and onOpenChange for control from outside the component. + const [_open, _setOpen] = createSignal(local.defaultOpen); + const open = () => local.open ?? _open(); + const setOpen = (value: boolean | ((value: boolean) => boolean)) => { + if (local.onOpenChange) { + return local.onOpenChange?.(typeof value === "function" ? value(open()) : value); + } + _setOpen(value); + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${open()}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }; + + // Helper to toggle the sidebar. + const toggleSidebar = () => { + return isMobile() ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }; + + // Adds a keyboard shortcut to toggle the sidebar. + createEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + onCleanup(() => window.removeEventListener("keydown", handleKeyDown)); + }); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = () => (open() ? "expanded" : "collapsed"); + + const contextValue = { + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar + }; + + return ( + +
    + {local.children} +
    +
    + ); +}; + +type SidebarProps = ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; +}; + +const Sidebar: Component = (rawProps) => { + const props = mergeProps( + { + side: "left", + variant: "sidebar", + collapsible: "offcanvas" + }, + rawProps + ); + const [local, others] = splitProps(props, ["side", "variant", "collapsible", "class", "children"]); + + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + return ( + + +
    + {local.children} +
    +
    + + + +
    {local.children}
    +
    +
    +
    + + + ); +}; + +export default CreatePoll; \ No newline at end of file diff --git a/surreal-poll/src/pages/Home.tsx b/surreal-poll/src/pages/Home.tsx new file mode 100644 index 0000000..21aedca --- /dev/null +++ b/surreal-poll/src/pages/Home.tsx @@ -0,0 +1,12 @@ +import type { Component } from 'solid-js'; + +const Home: Component = () => { + return ( +
    +

    Polling App

    +

    Welcome to the polling application

    +
    + ); +}; + +export default Home; \ No newline at end of file diff --git a/surreal-poll/src/pages/PollResults.tsx b/surreal-poll/src/pages/PollResults.tsx new file mode 100644 index 0000000..3e572d9 --- /dev/null +++ b/surreal-poll/src/pages/PollResults.tsx @@ -0,0 +1,17 @@ +import type { Component } from 'solid-js'; +import { useParams } from '@solidjs/router'; + +const PollResults: Component = () => { + const params = useParams(); + + + return ( +
    +

    Poll Results

    +

    Poll ID: {params.id}

    + {/* Add results display here */} +
    + ); +}; + +export default PollResults; \ No newline at end of file diff --git a/surreal-poll/src/pages/Signin.tsx b/surreal-poll/src/pages/Signin.tsx new file mode 100644 index 0000000..98e009a --- /dev/null +++ b/surreal-poll/src/pages/Signin.tsx @@ -0,0 +1,63 @@ +import { useNavigate } from "@solidjs/router"; +import { createSignal, JSX } from "solid-js"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; +import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field"; +import { useAuth } from "~/lib/providers/auth"; + +export function Signin(): JSX.Element { + + const { login } = useAuth(); + const navigate = useNavigate(); + + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + + const handleSubmit = async () => { + + try { + await login(email(), password()); + navigate("/"); + } catch(e) { + console.error(e); + } + }; + + return ( + + + + Signup for an account + + + Enter your email and password to create an account + + + + + + Email + + setEmail(e.target.value)} + /> + + + + Password + + setPassword(e.target.value)} + /> + + + + + ); +} \ No newline at end of file diff --git a/surreal-poll/src/pages/Signup.tsx b/surreal-poll/src/pages/Signup.tsx new file mode 100644 index 0000000..5df2b41 --- /dev/null +++ b/surreal-poll/src/pages/Signup.tsx @@ -0,0 +1,96 @@ +import { useNavigate } from "@solidjs/router"; +import { createSignal, JSX } from "solid-js"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; +import { TextField, TextFieldInput, TextFieldLabel } from "~/components/ui/text-field"; +import { useAuth } from "~/lib/providers/auth"; + +export function Signup(): JSX.Element { + + const navigate = useNavigate(); + const { register } = useAuth(); + + const [name, setName] = createSignal(""); + const [email, setEmail] = createSignal(""); + const [password, setPassword] = createSignal(""); + const [confirmPassword, setConfirmPassword] = createSignal(""); + const [error, setError] = createSignal(); + + const handleSubmit = async () => { + + if(password() !== confirmPassword()) { + setError("Passwords do not match"); + return; + } + + try { + await register({ + name: name(), + email: email(), + pass: password(), + }); + + navigate("/signin"); + } catch(e) { + setError("An error occurred while creating your account"); + } + } + + return ( + + + + Signup for an account + + + Enter your email and password to create an account + + + + + + Name + + setName(e.target.value)} + /> + + + + Email + + setEmail(e.target.value)} + /> + + + + Password + + setPassword(e.target.value)} + /> + + + + Confirm Password + + setConfirmPassword(e.target.value)} + /> + + + + + ); +} \ No newline at end of file diff --git a/surreal-poll/src/pages/VotePoll.tsx b/surreal-poll/src/pages/VotePoll.tsx new file mode 100644 index 0000000..dbd44db --- /dev/null +++ b/surreal-poll/src/pages/VotePoll.tsx @@ -0,0 +1,60 @@ +import { createSignal, For, onMount, Show, type Component } from 'solid-js'; +import { useParams } from '@solidjs/router'; +import { useSurreal } from '~/lib/providers/surrealdb'; +import { RecordId } from 'surrealdb'; +import { Poll } from '~/types/poll'; +import { PollQuestion } from '~/types/pollQuestion'; +import { getPollQuestions } from '~/lib/repositories/pollQuestion'; +import { getPoll } from '~/lib/repositories/poll'; +import { PollQuestionRow } from '~/components/Vote'; +import { votePollQuestion } from '~/lib/repositories/pollVote'; +import { useAuth } from '~/lib/providers/auth'; + +const VotePoll: Component = () => { + + const params = useParams(); + const { client } = useSurreal(); + const { user } = useAuth(); + + const [poll, setPoll] = createSignal(); + const [questions, setQuestions] = createSignal(); + + onMount(async () => { + const db = client(); + await db.ready; + + const record = new RecordId("poll", params.id); + const poll = await getPoll(db, record); + + setPoll(poll); + + const questions = await getPollQuestions(db, poll); + setQuestions(questions); + + console.log(questions); + }); + + const onVoteQuestion = async (question: PollQuestion) => { + const userRecord = user(); + const db = client(); + + if(!userRecord) { + return; + } + + votePollQuestion(db, userRecord.id, question.id); + }; + + return ( +
    +

    Vote on Poll

    + + {(question) => + + } + +
    + ); +}; + +export default VotePoll; \ No newline at end of file diff --git a/surreal-poll/src/styles/index.css b/surreal-poll/src/styles/index.css new file mode 100644 index 0000000..82e8f87 --- /dev/null +++ b/surreal-poll/src/styles/index.css @@ -0,0 +1,141 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --info: 204 94% 94%; + --info-foreground: 199 89% 48%; + + --success: 149 80% 90%; + --success-foreground: 160 84% 39%; + + --warning: 48 96% 89%; + --warning-foreground: 25 95% 53%; + + --error: 0 93% 94%; + --error-foreground: 0 84% 60%; + + --ring: 240 5.9% 10%; + + --radius: 0.5rem; + } + + .dark, + [data-kb-theme="dark"] { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --info: 204 94% 94%; + --info-foreground: 199 89% 48%; + + --success: 149 80% 90%; + --success-foreground: 160 84% 39%; + + --warning: 48 96% 89%; + --warning-foreground: 25 95% 53%; + + --error: 0 93% 94%; + --error-foreground: 0 84% 60%; + + --ring: 240 4.9% 83.9%; + + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: + "rlig" 1, + "calt" 1; + } +} + +@layer utilities { + .step { + counter-increment: step; + } + + .step:before { + @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; + @apply ml-[-50px] mt-[-4px]; + content: counter(step); + } +} + +@media (max-width: 640px) { + .container { + @apply px-4; + } +} + +::-webkit-scrollbar { + width: 16px; +} + +::-webkit-scrollbar-thumb { + border-radius: 9999px; + border: 4px solid transparent; + background-clip: content-box; + @apply bg-accent; +} + +::-webkit-scrollbar-corner { + display: none; +} diff --git a/surreal-poll/src/types/poll.ts b/surreal-poll/src/types/poll.ts new file mode 100644 index 0000000..f455fcb --- /dev/null +++ b/surreal-poll/src/types/poll.ts @@ -0,0 +1,12 @@ +import { RecordId } from "surrealdb"; + +export type Poll = { + id: RecordId; + title: string; + creator: RecordId<"user">; +} + +export type PollPayload = { + title: string; + questions: string[]; +}; \ No newline at end of file diff --git a/surreal-poll/src/types/pollHasQuestion.ts b/surreal-poll/src/types/pollHasQuestion.ts new file mode 100644 index 0000000..6d879db --- /dev/null +++ b/surreal-poll/src/types/pollHasQuestion.ts @@ -0,0 +1,7 @@ +import { RecordId } from "surrealdb"; + +export type PollHasQuestion = { + id: RecordId<"pollHasQuestion">; + in: RecordId<"poll">; + out: RecordId<"pollQuestion">; +}; \ No newline at end of file diff --git a/surreal-poll/src/types/pollQuestion.ts b/surreal-poll/src/types/pollQuestion.ts new file mode 100644 index 0000000..af3dc09 --- /dev/null +++ b/surreal-poll/src/types/pollQuestion.ts @@ -0,0 +1,6 @@ +import { RecordId } from "surrealdb"; + +export interface PollQuestion { + id: RecordId<"pollQuestion">; + question: string; +} \ No newline at end of file diff --git a/surreal-poll/src/types/pollVote.ts b/surreal-poll/src/types/pollVote.ts new file mode 100644 index 0000000..8318956 --- /dev/null +++ b/surreal-poll/src/types/pollVote.ts @@ -0,0 +1,7 @@ +import { RecordId } from "surrealdb"; + +export type PollVote = { + id: RecordId<"pollVote">; + in: RecordId<"user">; + out: RecordId<"">; +}; \ No newline at end of file diff --git a/surreal-poll/src/types/user.ts b/surreal-poll/src/types/user.ts new file mode 100644 index 0000000..eda0009 --- /dev/null +++ b/surreal-poll/src/types/user.ts @@ -0,0 +1,8 @@ +import { RecordId } from "surrealdb"; + +export interface UserRecord { + id: RecordId<"user">; + name: string; + email: string; + pass: string; +} \ No newline at end of file diff --git a/surreal-poll/src/vite-env.d.ts b/surreal-poll/src/vite-env.d.ts new file mode 100644 index 0000000..6d43c0d --- /dev/null +++ b/surreal-poll/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_DB_HOST: string; + readonly VITE_DB_USER: string; + readonly VITE_DB_PASS: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/surreal-poll/tailwind.config.js b/surreal-poll/tailwind.config.js new file mode 100644 index 0000000..ba12978 --- /dev/null +++ b/surreal-poll/tailwind.config.js @@ -0,0 +1,104 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["variant", [".dark &", '[data-kb-theme="dark"] &']], + content: ["./src/**/*.{ts,tsx}"], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px" + } + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))" + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))" + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))" + }, + info: { + DEFAULT: "hsl(var(--info))", + foreground: "hsl(var(--info-foreground))" + }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))" + }, + warning: { + DEFAULT: "hsl(var(--warning))", + foreground: "hsl(var(--warning-foreground))" + }, + error: { + DEFAULT: "hsl(var(--error))", + foreground: "hsl(var(--error-foreground))" + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))" + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))" + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))" + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))" + } + }, + borderRadius: { + xl: "calc(var(--radius) + 4px)", + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--kb-accordion-content-height)" } + }, + "accordion-up": { + from: { height: "var(--kb-accordion-content-height)" }, + to: { height: 0 } + }, + "content-show": { + from: { opacity: 0, transform: "scale(0.96)" }, + to: { opacity: 1, transform: "scale(1)" } + }, + "content-hide": { + from: { opacity: 1, transform: "scale(1)" }, + to: { opacity: 0, transform: "scale(0.96)" } + }, + "caret-blink": { + "0%,70%,100%": { opacity: "1" }, + "20%,50%": { opacity: "0" } + } + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "content-show": "content-show 0.2s ease-out", + "content-hide": "content-hide 0.2s ease-out", + "caret-blink": "caret-blink 1.25s ease-out infinite" + } + } + }, + plugins: [require("tailwindcss-animate")] +} diff --git a/surreal-poll/tsconfig.json b/surreal-poll/tsconfig.json new file mode 100644 index 0000000..fdb3bdc --- /dev/null +++ b/surreal-poll/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": [ + "vite/client" + ], + "noEmit": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "~/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/surreal-poll/ui.config.json b/surreal-poll/ui.config.json new file mode 100644 index 0000000..4e71eec --- /dev/null +++ b/surreal-poll/ui.config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://solid-ui.com/schema.json", + "tsx": true, + "tailwind": { + "css": "src/styles/index.css", + "config": "tailwind.config.js", + "prefix": "" + }, + "aliases": { + "components": "~/components/ui", + "utils": "~/lib/utils" + } +} \ No newline at end of file diff --git a/surreal-poll/vite.config.ts b/surreal-poll/vite.config.ts new file mode 100644 index 0000000..a1e3dd2 --- /dev/null +++ b/surreal-poll/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import solidPlugin from 'vite-plugin-solid'; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [solidPlugin()], + server: { + port: 3000, + }, + build: { + target: 'esnext', + }, + resolve: { + alias: { + "~": resolve(__dirname, "./src") + } + } +});