diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c061075..de82a8c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -17,6 +17,7 @@ const config = { "@typescript-eslint/array-type": "off", "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-non-null-asserted-optional-chain": ["off"], "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/consistent-type-imports": [ "warn", diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 9fbc5b9..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 800272b..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/parentgrine - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/parentgrine-server.iml b/.idea/kidarr-server.iml similarity index 100% rename from .idea/parentgrine-server.iml rename to .idea/kidarr-server.iml diff --git a/.idea/modules.xml b/.idea/modules.xml index a862b72..9fe0d47 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..94a25f7 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e3a678d..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Next.js: debug server-side", - "type": "node-terminal", - "request": "launch", - "command": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --config ./ssl-proxy.json" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 908255f..4001a19 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,26 @@ { - "cmake.configureOnOpen": false, - "workbench.editor.labelFormat": "short" + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + ".vscode": true, + ".next": true, + "node_modules": true + }, + "workbench.colorTheme": "poimandres-noitalics", + "sqltools.connections": [ + { + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "driver": "PostgreSQL", + "name": "kidarr (local)", + "database": "kidarr", + "username": "postgres", + "password": "hackme" + } + ] } diff --git a/README.md b/README.md index aa85ddd..fba19ed 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,28 @@ -# Kidarr ServerπŸš€ +# Create T3 App -Welcome to Kidarr, the ultimate child location tracking app that prioritises safety and peace of mind for parents! -🌟 +This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![GitHub issues](https://img.shields.io/github/issues/kid-arr/kidarr-server)](https://github.com/kid-arr/kidarr-server/issues) -[![GitHub stars](https://img.shields.io/github/stars/kid-arr/kidarr-server)](https://github.com/kid-arr/kidarr-server/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/kid-arr/kidarr-server)](https://github.com/kid-arr/kidarr-server/network) +## What's next? How do I make an app with this? -## Overview +We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. -Kidarr is a secure and user-friendly mobile application designed to help parents keep track of their children's -whereabouts in real-time. With advanced location tracking features, intuitive UI, and robust security measures, -Kidarr provides parents with the peace of mind they deserve. +If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. -## Features +- [Next.js](https://nextjs.org) +- [NextAuth.js](https://next-auth.js.org) +- [Prisma](https://prisma.io) +- [Tailwind CSS](https://tailwindcss.com) +- [tRPC](https://trpc.io) -- **Real-time Location Tracking**: Stay informed about your child's location at all times. +## Learn More -- **Geofencing**: Set up safe zones and receive notifications when your child enters or leaves predefined areas. +To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: -- **SOS Alert**: In case of an emergency, your child can send an SOS alert with their current location. +- [Documentation](https://create.t3.gg/) +- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) β€” Check out these awesome tutorials -- **History Tracking**: View a detailed history of your child's movement over time. +You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) β€” your feedback and contributions are welcome! -- **Privacy and Security**: We prioritize the privacy and security of your data. All communication is encrypted, and - location data is accessible only to authorized users. +## How do I deploy this? -## Getting Started - -### Prerequisites - -- iOS or Android device for your child -- Web, iOS or Android device for the parent - -### Installation - -1. Clone the repository: `git clone https://github.com/kid-arr/kidarr-server.git` -2. Follow the installation instructions in the [documentation](docs/INSTALL.md). - -## Usage - -1. Open the Kidarr app on your child's device. -2. Log in with your parent account. -3. Enjoy peace of mind by tracking your child's location. - -For more detailed instructions, check out our [User Guide](docs/UserGuide.md). - -## Contributing - -We welcome contributions from the community! Whether you're a developer, designer, or tester, your input is valuable. -Check out our [contribution guidelines](CONTRIBUTING.md) for more information. - -## Support - -If you encounter any issues or have questions, feel free -to [open an issue](https://github.com/kid-arr/kidarr-server/issues). We're here to help! - -## License - -Kidarr is licensed under the [MIT License](LICENSE). - -Happy tracking! 🌍✨ +Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/bun.lockb b/bun.lockb index 057101d..ca4ce00 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components.json b/components.json index e3c2bcf..33fb800 100644 --- a/components.json +++ b/components.json @@ -5,10 +5,9 @@ "tsx": true, "tailwind": { "config": "tailwind.config.ts", - "css": "styles/globals.css", - "baseColor": "zinc", - "cssVariables": true, - "prefix": "" + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true }, "aliases": { "components": "@/components", diff --git a/drizzle.config.ts b/drizzle.config.ts index 811ffc6..cab1b8b 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,15 +1,11 @@ -import type { Config } from 'drizzle-kit'; -import { env } from '@/env'; +import type { Config } from "drizzle-kit"; +import { env } from "@/env.mjs"; -if (!env.DATABASE_URL) { - throw new Error('DATABASE_URL is missing'); -} export default { - // schema: './src/db', - schema: './src/server/db/schema.ts', - out: './drizzle', - driver: 'pg', + schema: "./src/server/db/schema", + out: "./src/server/db/migrations", + driver: "pg", dbCredentials: { connectionString: env.DATABASE_URL, - }, -} satisfies Config; + } +} satisfies Config; \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json deleted file mode 100644 index 1952f15..0000000 --- a/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "5", - "dialect": "pg", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1705356382861, - "tag": "0000_wide_gravity", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/kirimase.config.json b/kirimase.config.json new file mode 100644 index 0000000..886951d --- /dev/null +++ b/kirimase.config.json @@ -0,0 +1,18 @@ +{ + "hasSrc": true, + "packages": [ + "trpc", + "shadcn-ui", + "drizzle", + "next-auth" + ], + "preferredPackageManager": "bun", + "t3": true, + "alias": "@", + "rootPath": "src/", + "componentLib": "shadcn-ui", + "driver": "pg", + "provider": "postgresjs", + "orm": "drizzle", + "auth": "next-auth" +} \ No newline at end of file diff --git a/package.json b/package.json index f0e56d8..7c76479 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,26 @@ { - "name": "kidarr", - "version": "0.1.5", + "name": "kidarr-server", + "version": "0.1.0", "private": true, "type": "module", "scripts": { "build": "next build", - "db:push": "dotenv drizzle-kit push:postgres", - "db:studio": "dotenv drizzle-kit studio", + "_dev": "next dev", "dev": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --config ./ssl-proxy.json", "lint": "next lint", - "start": "next start" + "start": "next start", + "db:generate": "drizzle-kit generate:pg", + "db:migrate": "tsx src/server/db/migrate.ts", + "db:seed": "tsx src/server/db/seed/seed.ts && tsx src/server/db/seed/auth.ts", + "db:drop": "drizzle-kit drop", + "db:pull": "drizzle-kit introspect:pg", + "db:studio": "drizzle-kit studio", + "db:check": "drizzle-kit check:pg" }, "dependencies": { - "@auth/drizzle-adapter": "^0.3.14", - "@faker-js/faker": "^8.3.1", + "@auth/core": "^0.22.0", + "@auth/drizzle-adapter": "^0.3.16", + "@faker-js/faker": "^8.4.0", "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -44,26 +51,26 @@ "@radix-ui/react-tooltip": "^1.0.7", "@t3-oss/env-nextjs": "^0.7.3", "@tanstack/react-query": "^4.36.1", - "@trpc/client": "^10.45.0", - "@trpc/next": "^10.45.0", - "@trpc/react-query": "^10.45.0", - "@trpc/server": "^10.45.0", - "@vercel/analytics": "^1.1.1", + "@trpc/client": "^10.43.6", + "@trpc/next": "^10.43.6", + "@trpc/react-query": "^10.43.6", + "@trpc/server": "^10.43.6", + "@types/leaflet": "^1.9.8", + "@types/react-leaflet": "^3.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^0.2.0", - "date-fns": "^3.2.0", + "date-fns": "^3.3.1", "drizzle-orm": "^0.29.3", - "embla-carousel-react": "^8.0.0-rc19", - "generate-api-key": "^1.0.2", + "drizzle-zod": "^0.5.1", + "embla-carousel-react": "^8.0.0-rc20", "http-status-codes": "^2.3.0", "leaflet": "^1.9.4", "local-ssl-proxy": "^2.0.5", - "lucide-react": "^0.309.0", + "lucide-react": "^0.314.0", "next": "^14.0.4", "next-auth": "^4.24.5", "next-themes": "^0.2.1", - "pg": "^8.11.3", "postgres": "^3.4.3", "react": "18.2.0", "react-day-picker": "^8.10.0", @@ -79,29 +86,30 @@ "superjson": "^2.2.1", "tailwind-merge": "^2.2.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.8.0", + "vaul": "^0.8.9", "zod": "^3.22.4" }, "devDependencies": { - "@types/leaflet": "^1.9.8", - "@next/eslint-plugin-next": "^14.0.4", - "@types/eslint": "^8.56.2", - "@types/node": "^20.11.0", - "@types/react": "^18.2.47", - "@types/react-dom": "^18.2.18", - "@typescript-eslint/eslint-plugin": "^6.18.1", - "@typescript-eslint/parser": "^6.18.1", - "autoprefixer": "^10.4.16", - "dotenv-cli": "^7.3.0", - "drizzle-kit": "^0.20.12", - "eslint": "^8.56.0", - "postcss": "^8.4.33", - "prettier": "^3.2.2", - "prettier-plugin-tailwindcss": "^0.5.11", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" + "@types/eslint": "^8.44.7", + "@types/node": "^18.17.0", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "autoprefixer": "^10.4.14", + "dotenv": "^16.3.2", + "drizzle-kit": "^0.20.13", + "eslint": "^8.54.0", + "eslint-config-next": "^14.0.4", + "pg": "^8.11.3", + "postcss": "^8.4.31", + "prettier": "^3.1.0", + "prettier-plugin-tailwindcss": "^0.5.7", + "tailwindcss": "^3.3.5", + "tsx": "^4.7.0", + "typescript": "^5.1.6" }, "ct3aMetadata": { - "initVersion": "7.25.0" + "initVersion": "7.25.2" } } diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png deleted file mode 100644 index cbd81c1..0000000 Binary files a/public/android-chrome-192x192.png and /dev/null differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png deleted file mode 100644 index 8d144a0..0000000 Binary files a/public/android-chrome-512x512.png and /dev/null differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png deleted file mode 100644 index a033189..0000000 Binary files a/public/apple-touch-icon.png and /dev/null differ diff --git a/public/favicon copy.ico b/public/favicon copy.ico deleted file mode 100644 index efb824e..0000000 Binary files a/public/favicon copy.ico and /dev/null differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png deleted file mode 100644 index c31bb38..0000000 Binary files a/public/favicon-16x16.png and /dev/null differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png deleted file mode 100644 index dc9110b..0000000 Binary files a/public/favicon-32x32.png and /dev/null differ diff --git a/public/img/default-avatar.png b/public/img/default-avatar.png deleted file mode 100644 index eafe9f4..0000000 Binary files a/public/img/default-avatar.png and /dev/null differ diff --git a/public/site.webmanifest b/public/site.webmanifest deleted file mode 100644 index 25e63ff..0000000 --- a/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Kidarr - radar for your kids", - "short_name": "kidarr", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/scripts/queries.sql b/scripts/queries.sql new file mode 100644 index 0000000..78422f2 --- /dev/null +++ b/scripts/queries.sql @@ -0,0 +1 @@ +select * From pings \ No newline at end of file diff --git a/scripts/reset.sh b/scripts/reset.sh index 9f4b23a..1050b78 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -11,9 +11,8 @@ dropdb -f --if-exists kidarr echo "Creating db" createdb kidarr -bunx drizzle-kit generate:pg --config=./drizzle.config.ts -bunx drizzle-kit push:pg --config=./drizzle.config.ts +bun db:generate +bun db:migrate # bun run src/db/migrate.ts -bun run ./src/server/db/scripts/seed.ts -bun run ./src/server/db/scripts/auth.ts +bun db:seed diff --git a/src/app/(app)/children/page.tsx b/src/app/(app)/children/page.tsx new file mode 100644 index 0000000..b2dd468 --- /dev/null +++ b/src/app/(app)/children/page.tsx @@ -0,0 +1,19 @@ +import ChildList from "@/components/children/child-list"; +import NewChildModal from "@/components/children/child-modal"; +import { api } from "@/trpc/server"; +import { checkAuth } from "@/lib/auth/utils"; + +export default async function Children() { + await checkAuth(); + const { children } = await api.children.getChildren.query(); + + return ( +
+
+

Here are your children.

+ +
+ +
+ ); +} diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx new file mode 100644 index 0000000..279597b --- /dev/null +++ b/src/app/(app)/dashboard/page.tsx @@ -0,0 +1,10 @@ +import DashboardPage from "@/components/pages/dashboard-page"; +import { checkAuth } from "@/lib/auth/utils"; +import React from "react"; + +const Dashboard = async () => { + await checkAuth(); + return ; +}; + +export default Dashboard; diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..30ec414 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { SiteHeader } from '@/components/header/site-header'; +import { SocketProvider } from '@/lib/services/realtime/socket-provider'; + +type DashboardLayoutProps = { + children?: React.ReactNode +} +const AppLayout = async ({ children }: DashboardLayoutProps) => { + return ( + +
+ +
{children}
+
+
+ ); + +}; +export default AppLayout; diff --git a/src/app/(debug)/debug/page.tsx b/src/app/(debug)/debug/page.tsx deleted file mode 100644 index f2070aa..0000000 --- a/src/app/(debug)/debug/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SecureDebugDetails } from "@/components/debug/SecureDebugDetails"; -import React from "react"; -import HeadersPrinter from "@/components/debug/HeadersPrinter"; - -const DebugPage = async () => { - return ( - <> -

- This is what we know -

-
- - -
- - ); -}; -export default DebugPage; diff --git a/src/app/(parent)/children/page.tsx b/src/app/(parent)/children/page.tsx deleted file mode 100644 index 6803a32..0000000 --- a/src/app/(parent)/children/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ChildrenList from '@/components/children/children-list'; -import { api } from '@/trpc/server'; - -const ChildrenPage = async () => { - const kids = await api.child.mine.query(); - return ; -}; - -export default ChildrenPage; diff --git a/src/app/(parent)/dashboard/page.tsx b/src/app/(parent)/dashboard/page.tsx deleted file mode 100644 index 09063c1..0000000 --- a/src/app/(parent)/dashboard/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { api } from '@/trpc/server'; -import ChildrenFilter from '@/components/children/children-filter'; -import dynamic from 'next/dynamic'; -import { MapViewTypeSelector } from '@/components/maps/map-viewtype-selector'; - -const Dashboard = async () => { - const kids = await api.child.mine.query(); - const Map = dynamic(() => import('@/components/maps/main-map'), { - ssr: false, - }); - return
-
- - -
-
- -
-
; -}; - -export default Dashboard; diff --git a/src/app/(parent)/layout.tsx b/src/app/(parent)/layout.tsx deleted file mode 100644 index 6344c39..0000000 --- a/src/app/(parent)/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { SiteHeader } from '@/components/header/site-header'; -import React from 'react'; -import { SocketProvider } from '@/lib/services/realtime/socket-provider'; - -type DashboardLayoutProps = { - children?: React.ReactNode -} -const DashboardLayout = async ({ children }: DashboardLayoutProps) => { - return ( - -
- -
{children}
-
-
- ); - -}; -export default DashboardLayout; diff --git a/src/app/account/AccountCard.tsx b/src/app/account/AccountCard.tsx new file mode 100644 index 0000000..2e37f10 --- /dev/null +++ b/src/app/account/AccountCard.tsx @@ -0,0 +1,45 @@ +import { Card } from "@/components/ui/card"; + +interface AccountCardProps { + params: { + header: string; + description: string; + price?: number; + }; + children: React.ReactNode; +} + +export function AccountCard({ params, children }: AccountCardProps) { + const { header, description } = params; + return ( + +
+

{header}

+

{description}

+
+ {children} +
+ ); +} + +export function AccountCardBody({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +export function AccountCardFooter({ + description, + children, +}: { + children: React.ReactNode; + description: string; +}) { + return ( + + ); +} diff --git a/src/app/account/UpdateEmailCard.tsx b/src/app/account/UpdateEmailCard.tsx new file mode 100644 index 0000000..99ec6af --- /dev/null +++ b/src/app/account/UpdateEmailCard.tsx @@ -0,0 +1,56 @@ +import { AccountCard, AccountCardFooter, AccountCardBody } from "./AccountCard"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/use-toast"; +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; + +export default function UpdateEmailCard({ email }: { email: string }) { + const { toast } = useToast(); + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + + const handleSubmit = async (event: React.SyntheticEvent) => { + event.preventDefault(); + const target = event.target as HTMLFormElement; + const form = new FormData(target); + const { email } = Object.fromEntries(form.entries()) as { email: string }; + if (email.length < 3) { + toast({ + description: "Email must be longer than 3 characters.", + variant: "destructive", + }); + return; + } + + startTransition(async () => { + const res = await fetch("/api/account", { + method: "PUT", + body: JSON.stringify({ email }), + headers: { "Content-Type": "application/json" }, + }); + if (res.status === 200) + toast({ description: "Successfully updated email!" }); + router.refresh(); + }); + }; + + return ( + +
+ + + + + + +
+
+ ); +} diff --git a/src/app/account/UpdateNameCard.tsx b/src/app/account/UpdateNameCard.tsx new file mode 100644 index 0000000..228cf03 --- /dev/null +++ b/src/app/account/UpdateNameCard.tsx @@ -0,0 +1,56 @@ +"use client"; +import { AccountCard, AccountCardFooter, AccountCardBody } from "./AccountCard"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/use-toast"; +import { useTransition } from "react"; +import { useRouter } from "next/navigation"; + +export default function UpdateNameCard({ name }: { name: string }) { + const { toast } = useToast(); + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + const handleSubmit = async (event: React.SyntheticEvent) => { + event.preventDefault(); + const target = event.target as HTMLFormElement; + const form = new FormData(target); + const { name } = Object.fromEntries(form.entries()) as { name: string }; + if (name.length < 3) { + toast({ + description: "Name must be longer than 3 characters.", + variant: "destructive", + }); + return; + } + + startTransition(async () => { + const res = await fetch("/api/account", { + method: "PUT", + body: JSON.stringify({ name }), + headers: { "Content-Type": "application/json" }, + }); + if (res.status === 200) + toast({ description: "Successfully updated name!" }); + router.refresh(); + }); + }; + + return ( + +
+ + + + + + +
+
+ ); +} diff --git a/src/app/account/UserSettings.tsx b/src/app/account/UserSettings.tsx new file mode 100644 index 0000000..9d38a40 --- /dev/null +++ b/src/app/account/UserSettings.tsx @@ -0,0 +1,17 @@ +"use client"; +import UpdateNameCard from "./UpdateNameCard"; +import UpdateEmailCard from "./UpdateEmailCard"; +import { AuthSession } from "@/lib/auth/utils"; + +export default function UserSettings({ + session, +}: { + session: AuthSession["session"]; +}) { + return ( + <> + + + + ); +} diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..0588287 --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,16 @@ +import UserSettings from "./UserSettings"; +import { checkAuth, getUserAuth } from "@/lib/auth/utils"; + +export default async function Account() { + await checkAuth(); + const { session } = await getUserAuth(); + + return ( +
+

Account

+
+ +
+
+ ); +} diff --git a/src/app/api/account/route.ts b/src/app/api/account/route.ts new file mode 100644 index 0000000..4f16cf9 --- /dev/null +++ b/src/app/api/account/route.ts @@ -0,0 +1,15 @@ +import { getUserAuth } from "@/lib/auth/utils"; +import { db } from "@/server/db/index"; +import { users } from "@/server/db/schema/auth"; +import { eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; + +export async function PUT(request: Request) { + const { session } = await getUserAuth(); + if (!session) return new Response("Error", { status: 400 }); + const body = (await request.json()) as { name?: string; email?: string }; + + await db.update(users).set({ ...body }).where(eq(users.id, session.user.id)); + revalidatePath("/account"); + return new Response(JSON.stringify({ message: "ok" }), { status: 200 }); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 1570f88..f9f233c 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,7 +1,14 @@ -import NextAuth from "next-auth"; +import { DefaultSession } from "next-auth"; +import NextAuth from "next-auth/next"; +import { authOptions } from "@/lib/auth/utils"; -import { authOptions } from "@/server/auth"; +declare module "next-auth" { + interface Session { + user: DefaultSession["user"] & { + id: string; + }; + } +} -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/src/app/api/child/register/route.ts b/src/app/api/child/register/route.ts deleted file mode 100644 index 8383880..0000000 --- a/src/app/api/child/register/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -export async function POST(req: Request) { - const body = await req.json(); - console.log("route", "register", body); -} diff --git a/src/app/api/child/route.ts b/src/app/api/child/route.ts deleted file mode 100644 index 014d00e..0000000 --- a/src/app/api/child/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from "@/server/db"; -import { child } from "@/server/db/schema"; -import { StatusCodes, getReasonPhrase } from "http-status-codes"; -import { NextResponse } from "next/server"; -import { eq } from "drizzle-orm"; -import { getServerAuthSession } from "@/server/auth"; - -export async function GET(request: Request) { - const session = await getServerAuthSession(); - if (!session || !session.user) - return NextResponse.json( - { error: getReasonPhrase(StatusCodes.UNAUTHORIZED) }, - { status: StatusCodes.UNAUTHORIZED }, - ); - const activeChildren = await db.query.child.findMany({ - where: eq(child.parentId, session.user.id), - with: { - devices: { - with: { pings: true }, - }, - }, - }); - - return NextResponse.json(activeChildren); -} diff --git a/src/app/api/device/connect/route.ts b/src/app/api/device/connect/route.ts deleted file mode 100644 index 353c95a..0000000 --- a/src/app/api/device/connect/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db } from '@/server/db'; -import { eq } from 'drizzle-orm'; -import { StatusCodes } from 'http-status-codes'; -import { child, device } from '@/server/db/schema'; -import { createApiKey } from '@/lib/services/auth/api'; -import { badRequest } from '@/app/api/responses'; - -type DeviceConnectRequest = { - deviceId: string; - childId: string; - deviceName: string; -} -const POST = async (req: Request, res: Response) => { - if (req.method === 'POST') { - const { deviceId, childId, deviceName } = await req.json() as DeviceConnectRequest; - console.log('route', 'childId', childId); - console.log('route', 'deviceId', deviceId); - - if (!childId || !deviceId) { - return badRequest('Invalid registration request'); - } - - const childToRegister = ( - await db.selectDistinct().from(child).where(eq(child.id, childId)) - )[0]; - - if (!childToRegister) { - return badRequest('Invalid registration request'); - } - - let done = false; - let pin = 2021; - while (!done) { - pin = Math.floor(1000 + Math.random() * 9000); - console.log('route', 'device/connect', 'checking for PIN', pin); - const exists = await db - .selectDistinct() - .from(device) - .where(eq(device.pin, pin)); - console.log('route', 'exists', exists); - done = exists.length === 0; - } - - const apiKey = createApiKey(); - await db - .insert(device) - .values({ - childId: childToRegister.id, - deviceId: deviceId, - deviceName: deviceName, - pin, - apiKey: apiKey, - }) - .execute(); - return Response.json( - { childId, deviceId, deviceName, pin, apiKey }, - { status: StatusCodes.CREATED }, - ); - } - return badRequest('Invalid registration request'); -}; - -export { POST }; diff --git a/src/app/api/responses/index.ts b/src/app/api/responses/index.ts deleted file mode 100644 index 5cd6bad..0000000 --- a/src/app/api/responses/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { StatusCodes } from 'http-status-codes'; - -export const notAuthorised = () => - new Response('Not authorised', { - status: StatusCodes.BAD_REQUEST, - }); -export const badRequest = (message: string) => - new Response(message, { - status: StatusCodes.BAD_REQUEST, - }); diff --git a/src/app/debug/page.tsx b/src/app/debug/page.tsx new file mode 100644 index 0000000..67335b0 --- /dev/null +++ b/src/app/debug/page.tsx @@ -0,0 +1,12 @@ +import { Button } from "@/components/ui/button"; +import React from "react"; + +const DebugPage = () => { + return ( +
+ +
+ ); +}; + +export default DebugPage; diff --git a/src/app/devices/page.tsx b/src/app/devices/page.tsx new file mode 100644 index 0000000..7003893 --- /dev/null +++ b/src/app/devices/page.tsx @@ -0,0 +1,19 @@ +import DeviceList from "@/components/devices/DeviceList"; +import NewDeviceModal from "@/components/devices/DeviceModal"; +import { api } from "@/trpc/server"; +import { checkAuth } from "@/lib/auth/utils"; + +export default async function Devices() { + await checkAuth(); + const { devices } = await api.devices.getDevices.query(); + + return ( +
+
+

Devices

+ +
+ +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 835e5d3..e096f50 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,26 +1,21 @@ import "@/styles/globals.css"; -import { Analytics } from "@vercel/analytics/react"; -import { ABeeZee as TheFont } from "next/font/google"; -import { cookies } from "next/headers"; + +import { Inter } from "next/font/google"; import { TRPCReactProvider } from "@/trpc/react"; -import { ThemeProvider } from "@/components/providers/theme-provider"; -import { type Metadata } from "next"; -import NextAuthProvider from "@/lib/services/auth/provider"; +import { ThemeProvider } from "@/components/ThemeProvider"; +import { Toaster } from "@/components/ui/toaster"; +import NextAuthProvider from "@/lib/auth/Provider"; -const inter = TheFont({ - weight: "400", +const inter = Inter({ subsets: ["latin"], variable: "--font-sans", }); -export const metadata: Metadata = { +export const metadata = { title: "Kidarr", - description: "Radar for your kids", - manifest: "/site.webmanifest", - icons: { - icon: "/favicon.ico", - }, + description: "Radarr for your kids", + icons: [{ rel: "icon", url: "/favicon.ico" }], }; export default function RootLayout({ @@ -29,16 +24,20 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + - - - - {children} - - - - + + + {children} + + + + ); diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..568eb6e --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,25 @@ +export default function Loading() { + return ( +
+
+ + Loading... +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3a2fa6e..6c5d549 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,17 @@ -import { getServerAuthSession } from '@/server/auth'; -import HomePage from '@/components/pages/home-page'; -import { redirect } from 'next/navigation'; + +import HomePage from "@/components/pages/home-page"; +import { getUserAuth } from "@/lib/auth/utils"; +import { redirect } from "next/navigation"; export default async function Home() { - const session = await getServerAuthSession(); - - if (session?.user) { - redirect('/dashboard'); + const { session } = await getUserAuth(); + if (session) { + redirect("/dashboard"); } - return ; + + return ( +
+ +
+ ); } diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..a801d9e --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useTheme } from "next-themes"; + +export default function Page() { + const { setTheme } = useTheme(); + return ( +
+

Settings

+
+
+

Appearance

+

+ Customize the appearance of the app. Automatically switch between + day and night themes. +

+
+ + + +
+
+ ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..26b5257 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,45 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { usePathname } from "next/navigation"; + +import { Button } from "@/components/ui/button"; + +import { AlignRight } from "lucide-react"; +import { defaultLinks } from "@/config/nav"; + +export default function Navbar() { + const [open, setOpen] = useState(false); + const pathname = usePathname(); + return ( +
+ + {open ? ( +
+
    + {defaultLinks.map((link) => ( +
  • setOpen(false)} className=""> + + {link.title} + +
  • + ))} +
+
+ ) : null} +
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..8d1d63f --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,55 @@ +import Link from "next/link"; + +import SidebarItems from "./SidebarItems"; +import { Avatar, AvatarFallback } from "./ui/avatar"; + +import { AuthSession, getUserAuth } from "@/lib/auth/utils"; + +const Sidebar = async () => { + const session = await getUserAuth(); + if (session.session === null) return null; + + return ( + + ); +}; + +export default Sidebar; + +const UserDetails = ({ session }: { session: AuthSession }) => { + if (session.session === null) return null; + const { user } = session.session; + + if (!user?.name || user.name.length == 0) return null; + + return ( + +
+
+

{user.name ?? "John Doe"}

+

+ {user.email ?? "john@doe.com"} +

+
+ + + {user.name + ? user.name + ?.split(" ") + .map((word) => word[0].toUpperCase()) + .join("") + : "~"} + + +
+ + ); +}; diff --git a/src/components/SidebarItems.tsx b/src/components/SidebarItems.tsx new file mode 100644 index 0000000..2569741 --- /dev/null +++ b/src/components/SidebarItems.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { LucideIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { defaultLinks, additionalLinks } from "@/config/nav"; + +export interface SidebarLink { + title: string; + href: string; + icon: LucideIcon; +} + +const SidebarItems = () => { + return ( + <> + + {additionalLinks.length > 0 + ? additionalLinks.map((l) => ( + + )) + : null} + + ); +}; +export default SidebarItems; + +const SidebarLinkGroup = ({ + links, + title, + border, +}: { + links: SidebarLink[]; + title?: string; + border?: boolean; +}) => { + const pathname = usePathname(); + + return ( +
+ {title ? ( +

+ {title} +

+ ) : null} +
    + {links.map((link) => ( +
  • + +
  • + ))} +
+
+ ); +}; +const SidebarLink = ({ + link, + active, +}: { + link: SidebarLink; + active: boolean; +}) => { + return ( + +
+
+ + {link.title} +
+ + ); +}; diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..b0ff266 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,9 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { type ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/src/components/children/child-form.tsx b/src/components/children/child-form.tsx new file mode 100644 index 0000000..8f015c4 --- /dev/null +++ b/src/components/children/child-form.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { + type Child, + type NewChildParams, + insertChildParams, +} from "@/server/db/schema/children"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api as trpc } from "@/trpc/react"; +import { Button } from "@/components/ui/button"; +import { type z } from "zod"; +import { useRouter } from "next/navigation"; +import { useToast } from "@/components/ui/use-toast"; + +const ChildForm = ({ + child, + closeModal, +}: { + child?: Child; + closeModal?: () => void; +}) => { + const { toast } = useToast(); + + const editing = !!child?.id; + + const router = useRouter(); + const utils = trpc.useContext(); + + const form = useForm>({ + // latest Zod release has introduced a TS error with zodResolver + // open issue: https://github.com/colinhacks/zod/issues/2663 + // errors locally but not in production + resolver: zodResolver(insertChildParams), + defaultValues: child ?? { + name: "", + email: "", + avatar: "", + }, + }); + + const onSuccess = async ( + action: "create" | "update" | "delete", + data?: { error?: string }, + ) => { + if (data?.error) { + toast({ + title: `${action + .slice(0, 1) + .toUpperCase() + .concat(action.slice(1))} Failed`, + description: data.error, + variant: "destructive", + }); + return; + } + + await utils.children.getChildren.invalidate(); + router.refresh(); + if (closeModal) closeModal(); + toast({ + title: "Success", + description: `Child ${action}d!`, + variant: "default", + }); + }; + + const { mutate: createChild, isLoading: isCreating } = + trpc.children.createChild.useMutation({ + onSuccess: (res) => onSuccess("create"), + }); + + const { mutate: updateChild, isLoading: isUpdating } = + trpc.children.updateChild.useMutation({ + onSuccess: (res) => onSuccess("update"), + }); + + const { mutate: deleteChild, isLoading: isDeleting } = + trpc.children.deleteChild.useMutation({ + onSuccess: (res) => onSuccess("delete"), + }); + + const handleSubmit = (values: NewChildParams) => { + if (editing) { + updateChild({ ...values, id: child.id }); + } else { + createChild(values); + } + }; + return ( +
+ + ( + + Name + + + + + + + )} + /> + ( + + Email + + + + + + + )} + /> + ( + + Avatar + + + + + + + )} + /> + + {editing ? ( + + ) : null} + + + ); +}; + +export default ChildForm; diff --git a/src/components/children/child-list.tsx b/src/components/children/child-list.tsx new file mode 100644 index 0000000..8ce3fbf --- /dev/null +++ b/src/components/children/child-list.tsx @@ -0,0 +1,73 @@ +"use client"; +import { type CompleteChild } from "@/server/db/schema/children"; +import { api as trpc } from "@/trpc/react"; +import ChildModal from "./child-modal"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Icons } from "@/components/icons"; +import ConnectDeviceDialog from "@/components/children/connect-device-dialog"; + +export default function ChildList({ children }: { children: CompleteChild[] }) { + const { data: c } = trpc.children.getChildren.useQuery(undefined, { + initialData: { children }, + refetchOnMount: false, + }); + + if (c.children.length === 0) { + return ; + } + + return ( + + + + Name + Last seen at + Actions + + + + {c.children?.map((kid) => )} + +
+ ); +} + +const Child = ({ child }: { child: CompleteChild }) => { + return ( + + {child.name} + Douglas + +
+ + +
+
+
+ ); +}; + +const EmptyState = () => { + return ( +
+

+ No children +

+

+ Get started by creating a new child. +

+
+ +
+
+ ); +}; diff --git a/src/components/children/child-modal.tsx b/src/components/children/child-modal.tsx new file mode 100644 index 0000000..e93f306 --- /dev/null +++ b/src/components/children/child-modal.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import ChildForm from "./child-form"; +import { type Child } from "@/server/db/schema/children"; +import { Icons } from "../icons"; + +export default function ChildModal({ + child, + emptyState, +}: { + child?: Child; + emptyState?: boolean; +}) { + const [open, setOpen] = useState(false); + const closeModal = () => setOpen(false); + const editing = !!child?.id; + return ( + + + {emptyState ? ( + + ) : ( + + )} + + + + {editing ? "Edit" : "Create"} Child + +
+ +
+
+
+ ); +} diff --git a/src/components/children/child-select-list.tsx b/src/components/children/child-select-list.tsx index f123c8c..00baa53 100644 --- a/src/components/children/child-select-list.tsx +++ b/src/components/children/child-select-list.tsx @@ -1,5 +1,5 @@ -'use client'; -import React from 'react'; +"use client"; +import React from "react"; import { Select, SelectContent, @@ -7,15 +7,15 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '@/components/ui/select'; -import type ChildModel from '@/lib/models/child'; +} from "@/components/ui/select"; +import { type Child } from "@/server/db/schema/children"; type ChildSelectListProps = { - kids: ChildModel[]; -} -const ChildSelectList: React.FC = ({ kids }) => { + children: Child[]; +}; +const ChildSelectList: React.FC = ({ children: kids }) => { return ( - @@ -23,10 +23,7 @@ const ChildSelectList: React.FC = ({ kids }) => { (All Children) {kids?.map((r) => ( - + {r.name} ))} diff --git a/src/components/children/children-filter.tsx b/src/components/children/children-filter.tsx index 0991ffc..7489fd5 100644 --- a/src/components/children/children-filter.tsx +++ b/src/components/children/children-filter.tsx @@ -1,17 +1,20 @@ -import React from 'react'; -import ChildSelectList from './child-select-list'; +"use client"; +import React, { useEffect } from "react"; +import ChildSelectList from "./child-select-list"; -import AddChildComponent from './add-child-component'; -import type ChildModel from '@/lib/models/child'; +import { type Child } from "@/server/db/schema/children"; type ChildrenFilterProps = { - kids: ChildModel[]; -} -const ChildrenFilter: React.FC = async ({ kids }) => { + children: Child[]; +}; +const ChildrenFilter: React.FC = ({ children }) => { + useEffect(() => { + console.log("ChildrenFilter: useEffect", children); + }, [children]); return ( -
- - +
+ + {/* */}
); }; diff --git a/src/components/children/connect-device-dialog.tsx b/src/components/children/connect-device-dialog.tsx index 339515c..3c5ddb0 100644 --- a/src/components/children/connect-device-dialog.tsx +++ b/src/components/children/connect-device-dialog.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Button } from '@/components/ui/button'; +import React from "react"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -8,21 +8,21 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from '@/components/ui/dialog'; +} from "@/components/ui/dialog"; -import { Icons } from '@/components/icons'; -import QRCode from 'react-qr-code'; -import type ChildModel from '@/lib/models/child'; +import { Icons } from "@/components/icons"; +import QRCode from "react-qr-code"; +import { type Child } from "@/server/db/schema/children"; type ConnectDeviceDialogProps = { - child: ChildModel; + child: Child; }; const ConnectDeviceDialog: React.FC = ({ child }) => { return ( - @@ -34,10 +34,10 @@ const ConnectDeviceDialog: React.FC = ({ child }) => {
-
+
diff --git a/src/components/debug/HeadersPrinter.tsx b/src/components/debug/HeadersPrinter.tsx deleted file mode 100644 index 3fdc952..0000000 --- a/src/components/debug/HeadersPrinter.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -import React from "react"; -import { headers } from "next/headers"; -import { getRequestHeaders } from "@/lib/helpers/headers"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; - -const HeadersPrinter = () => { - const header = headers(); - - const request = getRequestHeaders(header); - return ( - - Request Headers - {request} - - ); -}; -export default HeadersPrinter; diff --git a/src/components/debug/SecureDebugDetails.tsx b/src/components/debug/SecureDebugDetails.tsx deleted file mode 100644 index 4687cd0..0000000 --- a/src/components/debug/SecureDebugDetails.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import PrintEnv from "@/components/widgets/print-env"; -import { authOptions } from "@/server/auth"; -import { getServerSession } from "next-auth"; -import { headers } from "next/headers"; - -export const SecureDebugDetails = async () => { - const session = await getServerSession(authOptions); - const request = headers(); - return ; -}; diff --git a/src/components/devices/DeviceForm.tsx b/src/components/devices/DeviceForm.tsx new file mode 100644 index 0000000..4c24825 --- /dev/null +++ b/src/components/devices/DeviceForm.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { Device, NewDeviceParams, insertDeviceParams } from "@/server/db/schema/devices"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api as trpc } from "@/trpc/react"; +import { Button } from "@/components/ui/button"; +import { z } from "zod"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useRouter } from "next/navigation"; +import { useToast } from "@/components/ui/use-toast"; + +const DeviceForm = ({ + device, + closeModal, +}: { + device?: Device; + closeModal?: () => void; +}) => { + const { toast } = useToast(); + const { data: children } = trpc.children.getChildren.useQuery(); + const editing = !!device?.id; + + const router = useRouter(); + const utils = trpc.useContext(); + + const form = useForm>({ + // latest Zod release has introduced a TS error with zodResolver + // open issue: https://github.com/colinhacks/zod/issues/2663 + // errors locally but not in production + resolver: zodResolver(insertDeviceParams), + defaultValues: device ?? { + name: "", + deviceId: "", + childId: "" + }, + }); + + const onSuccess = async (action: "create" | "update" | "delete", + data?: { error?: string }, + ) => { + if (data?.error) { + toast({ + title: `${action + .slice(0, 1) + .toUpperCase() + .concat(action.slice(1))} Failed`, + description: data.error, + variant: "destructive", + }); + return; + } + + await utils.devices.getDevices.invalidate(); + router.refresh(); + if (closeModal) closeModal(); + toast({ + title: 'Success', + description: `Device ${action}d!`, + variant: "default", + }); + }; + + const { mutate: createDevice, isLoading: isCreating } = + trpc.devices.createDevice.useMutation({ + onSuccess: (res) => onSuccess("create"), + }); + + const { mutate: updateDevice, isLoading: isUpdating } = + trpc.devices.updateDevice.useMutation({ + onSuccess: (res) => onSuccess("update"), + }); + + const { mutate: deleteDevice, isLoading: isDeleting } = + trpc.devices.deleteDevice.useMutation({ + onSuccess: (res) => onSuccess("delete"), + }); + + const handleSubmit = (values: NewDeviceParams) => { + if (editing) { + updateDevice({ ...values, id: device.id }); + } else { + createDevice(values); + } + }; + return ( +
+ + ( + Name + + + + + + + )} + /> + ( + Device Id + + + + + + + )} + /> + ( + Child Id + + + + + + + )} + /> + + {editing ? ( + + ) : null} + + + ); +}; + +export default DeviceForm; diff --git a/src/components/devices/DeviceList.tsx b/src/components/devices/DeviceList.tsx new file mode 100644 index 0000000..04cdd86 --- /dev/null +++ b/src/components/devices/DeviceList.tsx @@ -0,0 +1,52 @@ +"use client"; +import { CompleteDevice } from "@/server/db/schema/devices"; +import { api as trpc } from "@/trpc/react"; +import DeviceModal from "./DeviceModal"; + + +export default function DeviceList({ devices }: { devices: CompleteDevice[] }) { + const { data: d } = trpc.devices.getDevices.useQuery(undefined, { + initialData: { devices }, + refetchOnMount: false, + }); + + if (d.devices.length === 0) { + return ; + } + + return ( +
    + {d.devices.map((device) => ( + + ))} +
+ ); +} + +const Device = ({ device }: { device: CompleteDevice }) => { + return ( +
  • +
    +
    {device.device.name}
    +
    + +
  • + ); +}; + +const EmptyState = () => { + return ( +
    +

    + No devices +

    +

    + Get started by creating a new device. +

    +
    + +
    +
    + ); +}; + diff --git a/src/components/devices/DeviceModal.tsx b/src/components/devices/DeviceModal.tsx new file mode 100644 index 0000000..e85aecb --- /dev/null +++ b/src/components/devices/DeviceModal.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../ui/dialog"; +import DeviceForm from "./DeviceForm"; +import { Device } from "@/server/db/schema/devices"; + +export default function DeviceModal({ + device, + emptyState, +}: { + device?: Device; + emptyState?: boolean; +}) { + const [open, setOpen] = useState(false); + const closeModal = () => setOpen(false); + const editing = !!device?.id; + return ( + + + { emptyState ? ( + + ) : ( + )} + + + + { editing ? "Edit" : "Create" } Device + +
    + +
    +
    +
    + ); +} diff --git a/src/components/forms/add-child-form.tsx b/src/components/forms/add-child-form.tsx deleted file mode 100644 index 471eade..0000000 --- a/src/components/forms/add-child-form.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import * as React from "react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import type * as z from "zod"; - -import { cn } from "@/lib/utils"; -import { newChildSchema } from "@/lib/validations/child"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { toast } from "@/components/ui/use-toast"; -import { Icons } from "@/components/icons"; -import { DialogFooter } from "../ui/dialog"; -import { - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { api } from "@/trpc/react"; -import { useRouter } from "next/navigation"; - -type AddChildFormProps = React.HTMLAttributes; -type FormData = z.infer; - -const AddChildForm: React.FC = ({ className, ...props }) => { - const router = useRouter(); - const [PIN, setPIN] = React.useState(""); - const utils = api.useUtils(); - const createChild = api.child.create.useMutation(); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ - resolver: zodResolver(newChildSchema), - }); - - const onSubmit = async (data: FormData) => { - try { - const result = await createChild.mutateAsync({ name: data.name }); - toast({ description: "Added new child" }); - await utils.child.invalidate(); - console.log("add-child-form", "onSubmit", createChild.data); - if (result) { - setPIN(result.id); - } else { - toast({ description: "Something went wrong", variant: "destructive" }); - } - } catch (err) { - toast({ description: "Something went wrong", variant: "destructive" }); - } - }; - - if (PIN) { - return ( - <> - Successfully added child -
    - {`Your child's PIN is ${PIN}`} - -
    - - ); - } - return ( -
    - - Add Child - - { - "Enter your child's details below and press save, then use the displayed PIN to register their device." - } - - -
    -
    -
    - - - {errors?.name && ( -

    {errors.name.message}

    - )} -
    -
    -
    - - - -
    -
    -
    - ); -}; -export default AddChildForm; diff --git a/src/components/forms/user-auth-form.tsx b/src/components/forms/user-auth-form.tsx index 05722bf..29e2679 100644 --- a/src/components/forms/user-auth-form.tsx +++ b/src/components/forms/user-auth-form.tsx @@ -1,19 +1,22 @@ "use client"; import * as React from "react"; -import { redirect, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { signIn } from "next-auth/react"; import { useForm } from "react-hook-form"; -import type * as z from "zod"; import { cn } from "@/lib/utils"; -import { userAuthSchema } from "@/lib/validations/auth"; import { buttonVariants } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "@/components/ui/use-toast"; import { Icons } from "@/components/icons"; +import { z } from "zod"; + +const userAuthSchema = z.object({ + email: z.string().email(), +}) type UserAuthFormProps = React.HTMLAttributes; diff --git a/src/components/header/auth-header.tsx b/src/components/header/auth-header.tsx index 6595ae6..b882252 100644 --- a/src/components/header/auth-header.tsx +++ b/src/components/header/auth-header.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { signIn, signOut, useSession } from "next-auth/react"; +import { buttonVariants } from "@/components/ui/button"; +import { signOut, useSession } from "next-auth/react"; import { DropdownMenu, DropdownMenuContent, @@ -29,8 +29,8 @@ const AuthHeader = () => { diff --git a/src/components/header/site-header.tsx b/src/components/header/site-header.tsx index 62fd3fd..65b1a52 100644 --- a/src/components/header/site-header.tsx +++ b/src/components/header/site-header.tsx @@ -13,45 +13,47 @@ import PresenceIndicator from "@/components/widgets/presence-indicator"; export function SiteHeader() { const { data: session, status } = useSession(); return ( -
    -
    - -
    - +
    + + Twitter +
    + + + + + +
    diff --git a/src/components/main-nav.tsx b/src/components/main-nav.tsx index 6c6cf92..6f68149 100644 --- a/src/components/main-nav.tsx +++ b/src/components/main-nav.tsx @@ -20,19 +20,17 @@ export function MainNav({ items }: MainNavProps) { {items?.length ? ( ) : null} diff --git a/src/components/maps/main-map.tsx b/src/components/maps/main-map.tsx index 8303d09..680594e 100644 --- a/src/components/maps/main-map.tsx +++ b/src/components/maps/main-map.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ "use client"; import "leaflet/dist/leaflet.css"; import React, { useEffect, useState } from "react"; @@ -10,119 +11,41 @@ import { Polyline, } from "react-leaflet"; import { usePingSocket } from "@/lib/hooks/use-ping-socket"; -import type ChildModel from "@/lib/models/child"; import MapMarker from "@/components/maps/map-marker"; import { getLatestPing } from "@/lib/helpers/location/ping"; +import { type CompleteChild } from "@/server/db/schema/children"; type MainMapProps = { - kids: ChildModel[]; + kids: CompleteChild[]; }; const MainMap: React.FC = ({ kids }) => { - const [isMounted, setIsMounted] = useState(false); - useEffect(() => { - setIsMounted(true); - console.log("MainMap", "kids", kids); - }, [kids]); - // Draw lines between each ping showing the direction of travel - const renderLines = (kids: ChildModel[]) => { - return kids?.map((kid) => - kid.devices?.map((device) => - device.pings?.map((ping, index) => { - const nextPing = device.pings[index + 1]; - if (nextPing) { - return ( - <> - - - - ); - } - return null; - }), - ), - ); - }; return ( - isMounted && ( -
    - - - <> - {renderLines(kids)} - {kids?.map((kid) => - kid.devices?.map((device) => { - const latestPing = getLatestPing(device.pings); - - return ( - latestPing && ( - - ) - ); - - // Draw coordinates of each ping on the map - const renderPings = () => { - return kids?.map((kid) => - kid.devices?.map((device) => - device.pings?.map((ping) => ( - - )), - ), - ); - }; - - return ( - latestPing && ( - - ) - ); - }), - )} - - -
    - ) +
    + + + {kids?.map((kid) => + kid.devices?.map((device) => + device.pings.map((ping) => ( + + )), + ), + )} + +
    ); }; export default MainMap; diff --git a/src/components/maps/map-viewtype-selector.tsx b/src/components/maps/map-viewtype-selector.tsx index fff1aa6..6ad6bb6 100644 --- a/src/components/maps/map-viewtype-selector.tsx +++ b/src/components/maps/map-viewtype-selector.tsx @@ -1,21 +1,24 @@ -'use client'; -import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; -import { Icons } from '@/components/icons'; -import React from 'react'; - +"use client"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { Icons } from "@/components/icons"; +import React from "react"; export const MapViewTypeSelector = () => { - const [currentView, setCurrentView] = React.useState('location'); + const [currentView, setCurrentView] = React.useState("location"); return ( - setCurrentView(value)}> + setCurrentView(value)} + > - + Location - + Route - ); + + ); }; diff --git a/src/components/pages/dashboard-page.tsx b/src/components/pages/dashboard-page.tsx new file mode 100644 index 0000000..d93a900 --- /dev/null +++ b/src/components/pages/dashboard-page.tsx @@ -0,0 +1,23 @@ +import dynamic from "next/dynamic"; +import { api } from "@/trpc/server"; +import ChildrenFilter from "../children/children-filter"; +import { MapViewTypeSelector } from "../maps/map-viewtype-selector"; + +const DashboardPage = async () => { + const { children } = await api.children.getChildren.query(); + const Map = dynamic(() => import("@/components/maps/main-map"), { + ssr: false, + }); + return ( +
    +
    + + +
    +
    + +
    +
    + ); +}; +export default DashboardPage; diff --git a/src/components/pages/home-page.tsx b/src/components/pages/home-page.tsx index 785a9aa..1c71444 100644 --- a/src/components/pages/home-page.tsx +++ b/src/components/pages/home-page.tsx @@ -6,95 +6,94 @@ import { Card, CardHeader, CardTitle, CardContent } from "../ui/card"; import Link from "next/link"; function HomePage() { - return ( - <> -
    -
    -

    Track Your Children with Ease

    -

    - Kidarr helps you keep an eye on your loved ones and ensure their - safety. -

    - - Let's go - -
    -
    + return ( + <> +
    +
    +

    Track Your Children with Ease

    +

    + Kidarr helps you keep an eye on your loved ones and ensure their + safety. +

    + + {`Let's go`} +
    +
    -
    -
    - - - Real-Time Location Tracking - - -

    - Instantly know where your children are at all times with - accurate GPS tracking. -

    -
    -
    - - - Geofencing Alerts - - -

    - Receive notifications when your child enters or leaves - designated safe zones. -

    -
    -
    - - - Activity Monitoring - - -

    - { - "View your child's activity history, including visited places and routes taken." - } -

    -
    -
    -
    -
    -
    -
    -

    - Keep Your Children Safe Today! -

    -

    - Download Kidarr now and stay connected with your loved ones. -

    +
    +
    + + + Real-Time Location Tracking + + +

    + Instantly know where your children are at all times with + accurate GPS tracking. +

    +
    +
    + + + Geofencing Alerts + + +

    + Receive notifications when your child enters or leaves + designated safe zones. +

    +
    +
    + + + Activity Monitoring + + +

    + { + "View your child's activity history, including visited places and routes taken." + } +

    +
    +
    +
    +
    +
    +
    +

    + Keep Your Children Safe Today! +

    +

    + Download Kidarr now and stay connected with your loved ones. +

    - - Download Now - -
    -
    -
    -

    - An open source experiment from PodNoms - source code available{" "} - - here - -

    -
    - - ); + + Download Now + +
    +
    +
    +

    + An open source experiment from PodNoms - source code available{" "} + + here + +

    +
    + + ); } export default HomePage; diff --git a/src/components/providers/theme-provider.tsx b/src/components/providers/theme-provider.tsx deleted file mode 100644 index d4b4bbf..0000000 --- a/src/components/providers/theme-provider.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client' - -import * as React from 'react' -import { ThemeProvider as NextThemesProvider } from 'next-themes' -import { type ThemeProviderProps } from 'next-themes/dist/types' - -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} -} diff --git a/src/components/ui/ThemeToggle.tsx b/src/components/ui/ThemeToggle.tsx new file mode 100644 index 0000000..63ff415 --- /dev/null +++ b/src/components/ui/ThemeToggle.tsx @@ -0,0 +1,40 @@ +"use client"; + +import * as React from "react"; +import { MoonIcon, SunIcon } from "lucide-react"; +import { useTheme } from "next-themes"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx index df0c256..ec505d0 100644 --- a/src/components/ui/carousel.tsx +++ b/src/components/ui/carousel.tsx @@ -2,18 +2,21 @@ import * as React from "react" import useEmblaCarousel, { - type EmblaCarouselType as CarouselApi, - type EmblaOptionsType as CarouselOptions, - type EmblaPluginType as CarouselPlugin, + type UseEmblaCarouselType, } from "embla-carousel-react" import { ArrowLeft, ArrowRight } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" +type CarouselApi = UseEmblaCarouselType[1] +type UseCarouselParameters = Parameters +type CarouselOptions = UseCarouselParameters[0] +type CarouselPlugin = UseCarouselParameters[1] + type CarouselProps = { opts?: CarouselOptions - plugins?: CarouselPlugin[] + plugins?: CarouselPlugin orientation?: "horizontal" | "vertical" setApi?: (api: CarouselApi) => void } diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index 6e011c6..ea40d19 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -12,6 +12,7 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( {...props} /> ) +Pagination.displayName = "Pagination" const PaginationContent = React.forwardRef< HTMLUListElement, @@ -44,19 +45,17 @@ const PaginationLink = ({ size = "icon", ...props }: PaginationLinkProps) => ( - - - + ) PaginationLink.displayName = "PaginationLink" @@ -90,6 +89,7 @@ const PaginationNext = ({ ) +PaginationNext.displayName = "PaginationNext" const PaginationEllipsis = ({ className, @@ -104,6 +104,7 @@ const PaginationEllipsis = ({ More pages ) +PaginationEllipsis.displayName = "PaginationEllipsis" export { Pagination, diff --git a/src/config/nav.ts b/src/config/nav.ts new file mode 100644 index 0000000..950b870 --- /dev/null +++ b/src/config/nav.ts @@ -0,0 +1,15 @@ +import { SidebarLink } from "@/components/SidebarItems"; +import { Cog, Globe, HomeIcon } from "lucide-react"; + +type AdditionalLinks = { + title: string; + links: SidebarLink[]; +}; + +export const defaultLinks: SidebarLink[] = [ + { href: "/", title: "Home", icon: HomeIcon }, + { href: "/account", title: "Account", icon: Cog }, + { href: "/settings", title: "Settings", icon: Cog }, +]; + +export const additionalLinks: AdditionalLinks[] = []; diff --git a/src/env.js b/src/env.js index ade7aad..5c2f937 100644 --- a/src/env.js +++ b/src/env.js @@ -1,5 +1,5 @@ -import { createEnv } from '@t3-oss/env-nextjs'; -import { z } from 'zod'; +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; export const env = createEnv({ /** @@ -7,30 +7,7 @@ export const env = createEnv({ * isn't built with invalid env vars. */ server: { - DATABASE_URL: z - .string() - .url() - .refine( - (str) => !str.includes('YOUR_PG_URL_HERE'), - 'You forgot to change the default URL', - ), - NODE_ENV: z - .enum(['development', 'test', 'production']) - .default('development'), - NEXTAUTH_SECRET: - process.env.NODE_ENV === 'production' - ? z.string() - : z.string().optional(), - NEXTAUTH_URL: z.preprocess( - // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL - // Since NextAuth.js automatically uses the VERCEL_URL if present. - (str) => process.env.VERCEL_URL ?? str, - // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string() : z.string().url(), - ), - GOOGLE_CLIENT_ID: z.string(), - GOOGLE_CLIENT_SECRET: z.string(), - ALLOWED_DEBUG_IP: z.string().optional(), + NODE_ENV: z.enum(["development", "test", "production"]), }, /** @@ -47,13 +24,8 @@ export const env = createEnv({ * middlewares) or client-side so we need to destruct manually. */ runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, - NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, - NEXTAUTH_URL: process.env.NEXTAUTH_URL, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - ALLOWED_DEBUG_IP: process.env.ALLOWED_DEBUG_IP, + // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/src/env.mjs b/src/env.mjs new file mode 100644 index 0000000..484d66e --- /dev/null +++ b/src/env.mjs @@ -0,0 +1,36 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + DATABASE_URL: z.string().min(1), + + NEXTAUTH_SECRET: process.env.NODE_ENV === "production" + ? z.string().min(1) + : z.string().min(1).optional(), + NEXTAUTH_URL: z.preprocess( + // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL + // Since NextAuth.js automatically uses the VERCEL_URL if present. + (str) => process.env.VERCEL_URL ?? str, + // VERCEL_URL doesn't include `https` so it cant be validated as a URL + process.env.VERCEL_URL ? z.string().min(1) : z.string().url() + ), + GOOGLE_CLIENT_ID: z.string().min(1), + GOOGLE_CLIENT_SECRET: z.string().min(1), + }, + client: { + // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1), + }, + // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually + // runtimeEnv: { + // DATABASE_URL: process.env.DATABASE_URL, + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + // }, + // For Next.js >= 13.4.4, you only need to destructure client variables: + experimental__runtimeEnv: { + // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + }, +}); diff --git a/src/lib/api/children/mutations.ts b/src/lib/api/children/mutations.ts new file mode 100644 index 0000000..51ba174 --- /dev/null +++ b/src/lib/api/children/mutations.ts @@ -0,0 +1,58 @@ +import { db } from "@/server/db/index"; +import { and, eq } from "drizzle-orm"; +import { + ChildId, + NewChildParams, + UpdateChildParams, + updateChildSchema, + insertChildSchema, + children, + childIdSchema +} from "@/server/db/schema/children"; +import { getUserAuth } from "@/lib/auth/utils"; + +export const createChild = async (child: NewChildParams) => { + const { session } = await getUserAuth(); + const newChild = insertChildSchema.parse({ ...child, userId: session?.user.id! }); + try { + const [c] = await db.insert(children).values(newChild).returning(); + return { child: c }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const updateChild = async (id: ChildId, child: UpdateChildParams) => { + const { session } = await getUserAuth(); + const { id: childId } = childIdSchema.parse({ id }); + const newChild = updateChildSchema.parse({ ...child, userId: session?.user.id! }); + try { + const [c] = await db + .update(children) + .set(newChild) + .where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!))) + .returning(); + return { child: c }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const deleteChild = async (id: ChildId) => { + const { session } = await getUserAuth(); + const { id: childId } = childIdSchema.parse({ id }); + try { + const [c] = await db.delete(children).where(and(eq(children.id, childId!), eq(children.userId, session?.user.id!))) + .returning(); + return { child: c }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + diff --git a/src/lib/api/children/queries.ts b/src/lib/api/children/queries.ts new file mode 100644 index 0000000..dc0c7ca --- /dev/null +++ b/src/lib/api/children/queries.ts @@ -0,0 +1,46 @@ +import { eq, and } from "drizzle-orm"; +import { getUserAuth } from "@/lib/auth/utils"; +import { db } from "@/server/db"; +import { + type ChildId, + childIdSchema, + children, +} from "@/server/db/schema/children"; + +export const getChildren = async () => { + const { session } = await getUserAuth(); + console.log("queries", "getChildren", session?.user.id!); + const c = await db.query.children.findMany({ + where: (children, { eq }) => eq(children.userId, session?.user.id!), + orderBy: (children) => children.name, + with: { + devices: { + with: { + pings: true, + }, + }, + }, + }); + console.log("queries", "gotChildren", c); + return { children: c }; + // const c = await db + // .select() + // .from(children) + // .where(eq(children.userId, session?.user.id!)) + // .innerJoin(devices, eq(devices.childId, children.id)) + // .innerJoin(pings, eq(pings.deviceId, devices.id)) + // .orderBy(children.name); + // return c; +}; + +export const getChildById = async (id: ChildId) => { + const { session } = await getUserAuth(); + const { id: childId } = childIdSchema.parse({ id }); + const [c] = await db + .select() + .from(children) + .where( + and(eq(children.id, childId), eq(children.userId, session?.user.id!)), + ); + return { child: c }; +}; diff --git a/src/lib/api/devices/mutations.ts b/src/lib/api/devices/mutations.ts new file mode 100644 index 0000000..64cca10 --- /dev/null +++ b/src/lib/api/devices/mutations.ts @@ -0,0 +1,58 @@ +import { db } from "@/server/db/index"; +import { and, eq } from "drizzle-orm"; +import { + DeviceId, + NewDeviceParams, + UpdateDeviceParams, + updateDeviceSchema, + insertDeviceSchema, + devices, + deviceIdSchema +} from "@/server/db/schema/devices"; +import { getUserAuth } from "@/lib/auth/utils"; + +export const createDevice = async (device: NewDeviceParams) => { + const { session } = await getUserAuth(); + const newDevice = insertDeviceSchema.parse({ ...device, userId: session?.user.id! }); + try { + const [d] = await db.insert(devices).values(newDevice).returning(); + return { device: d }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const updateDevice = async (id: DeviceId, device: UpdateDeviceParams) => { + const { session } = await getUserAuth(); + const { id: deviceId } = deviceIdSchema.parse({ id }); + const newDevice = updateDeviceSchema.parse({ ...device, userId: session?.user.id! }); + try { + const [d] = await db + .update(devices) + .set(newDevice) + .where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!))) + .returning(); + return { device: d }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const deleteDevice = async (id: DeviceId) => { + const { session } = await getUserAuth(); + const { id: deviceId } = deviceIdSchema.parse({ id }); + try { + const [d] = await db.delete(devices).where(and(eq(devices.id, deviceId!), eq(devices.userId, session?.user.id!))) + .returning(); + return { device: d }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + diff --git a/src/lib/api/devices/queries.ts b/src/lib/api/devices/queries.ts new file mode 100644 index 0000000..7396e15 --- /dev/null +++ b/src/lib/api/devices/queries.ts @@ -0,0 +1,19 @@ +import { db } from "@/server/db/index"; +import { eq, and } from "drizzle-orm"; +import { getUserAuth } from "@/lib/auth/utils"; +import { type DeviceId, deviceIdSchema, devices } from "@/server/db/schema/devices"; +import { children } from "@/server/db/schema/children"; + +export const getDevices = async () => { + const { session } = await getUserAuth(); + const d = await db.select({ device: devices, child: children }).from(devices).leftJoin(children, eq(devices.childId, children.id)).where(eq(devices.userId, session?.user.id!)); + return { devices: d }; +}; + +export const getDeviceById = async (id: DeviceId) => { + const { session } = await getUserAuth(); + const { id: deviceId } = deviceIdSchema.parse({ id }); + const [d] = await db.select().from(devices).where(and(eq(devices.id, deviceId), eq(devices.userId, session?.user.id!))).leftJoin(children, eq(devices.childId, children.id)); + return { device: d }; +}; + diff --git a/src/lib/api/pings/mutations.ts b/src/lib/api/pings/mutations.ts new file mode 100644 index 0000000..a1bb16c --- /dev/null +++ b/src/lib/api/pings/mutations.ts @@ -0,0 +1,58 @@ +import { db } from "@/server/db/index"; +import { and, eq } from "drizzle-orm"; +import { + PingId, + NewPingParams, + UpdatePingParams, + updatePingSchema, + insertPingSchema, + pings, + pingIdSchema +} from "@/server/db/schema/pings"; +import { getUserAuth } from "@/lib/auth/utils"; + +export const createPing = async (ping: NewPingParams) => { + const { session } = await getUserAuth(); + const newPing = insertPingSchema.parse({ ...ping, userId: session?.user.id! }); + try { + const [p] = await db.insert(pings).values(newPing).returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const updatePing = async (id: PingId, ping: UpdatePingParams) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + const newPing = updatePingSchema.parse({ ...ping, userId: session?.user.id! }); + try { + const [p] = await db + .update(pings) + .set(newPing) + .where(and(eq(pings.id, pingId!), eq(pings.userId, session?.user.id!))) + .returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + +export const deletePing = async (id: PingId) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + try { + const [p] = await db.delete(pings).where(and(eq(pings.id, pingId!), eq(pings.userId, session?.user.id!))) + .returning(); + return { ping: p }; + } catch (err) { + const message = (err as Error).message ?? "Error, please try again"; + console.error(message); + throw { error: message }; + } +}; + diff --git a/src/lib/api/pings/queries.ts b/src/lib/api/pings/queries.ts new file mode 100644 index 0000000..6263f33 --- /dev/null +++ b/src/lib/api/pings/queries.ts @@ -0,0 +1,19 @@ +import { db } from "@/server/db/index"; +import { eq, and } from "drizzle-orm"; +import { getUserAuth } from "@/lib/auth/utils"; +import { type PingId, pingIdSchema, pings } from "@/server/db/schema/pings"; +import { devices } from "@/server/db/schema/devices"; + +export const getPings = async () => { + const { session } = await getUserAuth(); + const p = await db.select({ ping: pings, device: devices }).from(pings).leftJoin(devices, eq(pings.deviceId, devices.id)).where(eq(pings.userId, session?.user.id!)); + return { pings: p }; +}; + +export const getPingById = async (id: PingId) => { + const { session } = await getUserAuth(); + const { id: pingId } = pingIdSchema.parse({ id }); + const [p] = await db.select().from(pings).where(and(eq(pings.id, pingId), eq(pings.userId, session?.user.id!))).leftJoin(devices, eq(pings.deviceId, devices.id)); + return { ping: p }; +}; + diff --git a/src/lib/auth/Provider.tsx b/src/lib/auth/Provider.tsx new file mode 100644 index 0000000..b2ab516 --- /dev/null +++ b/src/lib/auth/Provider.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +type Props = { + children?: React.ReactNode; +}; + +export default function NextAuthProvider({ children }: Props) { + return {children}; +}; \ No newline at end of file diff --git a/src/lib/auth/utils.ts b/src/lib/auth/utils.ts new file mode 100644 index 0000000..f8c57fe --- /dev/null +++ b/src/lib/auth/utils.ts @@ -0,0 +1,50 @@ +import { db } from "@/server/db/index"; +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { DefaultSession, getServerSession, NextAuthOptions } from "next-auth"; +import { redirect } from "next/navigation"; +import { env } from "@/env.mjs"; +import GoogleProvider from "next-auth/providers/google"; + +declare module "next-auth" { + interface Session { + user: DefaultSession["user"] & { + id: string; + }; + } +} + +export type AuthSession = { + session: { + user: { + id: string; + name?: string; + email?: string; + }; + } | null; +}; + +export const authOptions: NextAuthOptions = { + adapter: DrizzleAdapter(db), + callbacks: { + session: ({ session, user }) => { + session.user.id = user.id; + return session; + }, + }, + providers: [ + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, + }), + ], +}; + +export const getUserAuth = async () => { + const session = await getServerSession(authOptions); + return { session } as AuthSession; +}; + +export const checkAuth = async () => { + const { session } = await getUserAuth(); + if (!session) redirect("/signin"); +}; diff --git a/src/lib/hooks/useValidatedForm.tsx b/src/lib/hooks/useValidatedForm.tsx new file mode 100644 index 0000000..b5c8595 --- /dev/null +++ b/src/lib/hooks/useValidatedForm.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { ZodSchema } from "zod"; + +type EntityZodErrors = Partial>; + +export function useValidatedForm(insertEntityZodSchema: ZodSchema) { + const [errors, setErrors] = useState | null>(null); + const hasErrors = + errors !== null && + Object.values(errors).some((error) => error !== undefined); + + const handleChange = (event: FormEvent) => { + const target = event.target as EventTarget; + if ( + target instanceof HTMLInputElement || + target instanceof HTMLSelectElement || + target instanceof HTMLTextAreaElement + ) { + if (!(target instanceof HTMLInputElement && target.type === "submit")) { + const field = target.name as keyof Entity; + const result = insertEntityZodSchema.safeParse({ + [field]: target.value, + }); + const fieldError = result.success + ? undefined + : result.error.flatten().fieldErrors[field]; + + setErrors((prev) => ({ + ...prev, + [field]: fieldError, + })); + } + } + }; + return { errors, setErrors, handleChange, hasErrors }; +} diff --git a/src/lib/models/child.ts b/src/lib/models/child.ts deleted file mode 100644 index bc3785f..0000000 --- a/src/lib/models/child.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type DeviceModel from './device'; - -export default interface ChildModel { - id: string; - name: string; - avatar: string | null; - devices: DeviceModel[]; - // recentLocations: Location[]; -} diff --git a/src/lib/models/device.ts b/src/lib/models/device.ts deleted file mode 100644 index 0d4eb96..0000000 --- a/src/lib/models/device.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type PingModel from './ping'; - -export default interface DeviceModel { - id: string; - deviceName: string; - pings: PingModel[]; -} diff --git a/src/lib/models/location-update.ts b/src/lib/models/location-update.ts index 8501475..713c053 100644 --- a/src/lib/models/location-update.ts +++ b/src/lib/models/location-update.ts @@ -1,4 +1,4 @@ -import type Location from './location'; +import type Location from "./location"; export default interface LocationUpdate { childId: string; diff --git a/src/lib/models/ping.ts b/src/lib/models/ping.ts deleted file mode 100644 index 2d3384b..0000000 --- a/src/lib/models/ping.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface ChildModel { - id: string; - latitude: number; - longitude: number; - timestamp: Date; -} diff --git a/src/lib/services/auth/api.ts b/src/lib/services/auth/api.ts deleted file mode 100644 index 6a78e1b..0000000 --- a/src/lib/services/auth/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import generateApiKey from 'generate-api-key'; - -const createApiKey = () => - generateApiKey({ method: 'string', length: 256 }) as string; - -export { createApiKey }; diff --git a/src/lib/services/auth/provider.tsx b/src/lib/services/auth/provider.tsx deleted file mode 100644 index b8176c8..0000000 --- a/src/lib/services/auth/provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import { SessionProvider } from "next-auth/react"; -import { type ReactNode } from "react"; - -export default function NextAuthProvider({ - children, -}: { - children: ReactNode; -}) { - return {children}; -} diff --git a/src/lib/types/nav.ts b/src/lib/types/nav.ts deleted file mode 100644 index b19758e..0000000 --- a/src/lib/types/nav.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type NavItem = { - title: string; - href?: string; - disabled?: boolean; - external?: boolean; -}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 365058c..bd0c391 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)) } diff --git a/src/lib/validations/auth.ts b/src/lib/validations/auth.ts deleted file mode 100644 index 218890b..0000000 --- a/src/lib/validations/auth.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as z from 'zod' - -export const userAuthSchema = z.object({ - email: z.string().email(), -}) diff --git a/src/lib/validations/child.ts b/src/lib/validations/child.ts deleted file mode 100644 index 2354154..0000000 --- a/src/lib/validations/child.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as z from 'zod'; - -export const newChildSchema = z.object({ - name: z.string().max(50), -}); diff --git a/src/lib/validations/connect-device.ts b/src/lib/validations/connect-device.ts deleted file mode 100644 index cae18e4..0000000 --- a/src/lib/validations/connect-device.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as z from 'zod'; - -export const connectDeviceSchema = z.object({ - childId: z.string().max(50), -}); diff --git a/src/server/api/root.ts b/src/server/api/root.ts index b7ae28c..9f67ced 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,5 +1,7 @@ -import { childRouter } from "@/server/api/routers/child"; +import { postRouter } from "@/server/api/routers/post"; import { createTRPCRouter } from "@/server/api/trpc"; +import { childrenRouter } from "./routers/children"; +import { devicesRouter } from "./routers/devices"; /** * This is the primary router for your server. @@ -7,7 +9,9 @@ import { createTRPCRouter } from "@/server/api/trpc"; * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ - child: childRouter, + post: postRouter, + children: childrenRouter, + devices: devicesRouter, }); // export type definition of API diff --git a/src/server/api/routers/child.ts b/src/server/api/routers/child.ts deleted file mode 100644 index 20b081b..0000000 --- a/src/server/api/routers/child.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from 'zod'; - -import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc'; -import { child } from '@/server/db/schema'; -import { eq } from 'drizzle-orm'; - -export const childRouter = createTRPCRouter({ - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - console.log('Child', 'Create', ctx.session); - const c = { - parentId: ctx.session.user.id, - name: input.name, - }; - const result = await ctx.db.insert(child).values(c).returning(); - return result[0]; - }), - - mine: protectedProcedure.query(async ({ ctx }) => { - return ctx.db.query.child.findMany({ - orderBy: (child, { asc }) => [asc(child.name)], - where: eq(child.parentId, ctx.session.user.id), - with: { - devices: { - with: { pings: true }, - }, - }, - }); - }), -}); diff --git a/src/server/api/routers/children.ts b/src/server/api/routers/children.ts new file mode 100644 index 0000000..00dbd75 --- /dev/null +++ b/src/server/api/routers/children.ts @@ -0,0 +1,41 @@ +import { getChildById, getChildren } from "@/lib/api/children/queries"; +import { + publicProcedure, + createTRPCRouter, +} from "@/server/api/trpc"; +import { + childIdSchema, + insertChildParams, + updateChildParams, +} from "@/server/db/schema/children"; +import { + createChild, + deleteChild, + updateChild, +} from "@/lib/api/children/mutations"; + +export const childrenRouter = createTRPCRouter({ + getChildren: publicProcedure.query(async () => { + return getChildren(); + }), + getChildById: publicProcedure + .input(childIdSchema) + .query(async ({ input }) => { + return getChildById(input.id); + }), + createChild: publicProcedure + .input(insertChildParams) + .mutation(async ({ input }) => { + return createChild(input); + }), + updateChild: publicProcedure + .input(updateChildParams) + .mutation(async ({ input }) => { + return updateChild(input.id, input); + }), + deleteChild: publicProcedure + .input(childIdSchema) + .mutation(async ({ input }) => { + return deleteChild(input.id); + }), +}); diff --git a/src/server/api/routers/devices.ts b/src/server/api/routers/devices.ts new file mode 100644 index 0000000..16dd03f --- /dev/null +++ b/src/server/api/routers/devices.ts @@ -0,0 +1,32 @@ +import { getDeviceById, getDevices } from "@/lib/api/devices/queries"; +import { publicProcedure, createTRPCRouter } from "@/server/api/trpc"; +import { + deviceIdSchema, + insertDeviceParams, + updateDeviceParams, +} from "@/server/db/schema/devices"; +import { createDevice, deleteDevice, updateDevice } from "@/lib/api/devices/mutations"; + +export const devicesRouter = createTRPCRouter({ + getDevices: publicProcedure.query(async () => { + return getDevices(); + }), + getDeviceById: publicProcedure.input(deviceIdSchema).query(async ({ input }) => { + return getDeviceById(input.id); + }), + createDevice: publicProcedure + .input(insertDeviceParams) + .mutation(async ({ input }) => { + return createDevice(input); + }), + updateDevice: publicProcedure + .input(updateDeviceParams) + .mutation(async ({ input }) => { + return updateDevice(input.id, input); + }), + deleteDevice: publicProcedure + .input(deviceIdSchema) + .mutation(async ({ input }) => { + return deleteDevice(input.id); + }), +}); diff --git a/src/server/api/routers/post.ts b/src/server/api/routers/post.ts new file mode 100644 index 0000000..88f2fc7 --- /dev/null +++ b/src/server/api/routers/post.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; + +let post = { + id: 1, + name: "Hello World", +}; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + post = { id: post.id + 1, name: input.name }; + return post; + }), + + getLatest: publicProcedure.query(() => { + return post; + }), +}); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 8b2d57d..213e30b 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -6,14 +6,10 @@ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will * need to use are documented accordingly near the end. */ - -import { initTRPC, TRPCError } from "@trpc/server"; +import { initTRPC } from "@trpc/server"; import superjson from "superjson"; import { ZodError } from "zod"; -import { getServerAuthSession } from "@/server/auth"; -import { db } from "@/server/db"; - /** * 1. CONTEXT * @@ -27,11 +23,7 @@ import { db } from "@/server/db"; * @see https://trpc.io/docs/server/context */ export const createTRPCContext = async (opts: { headers: Headers }) => { - const session = await getServerAuthSession(); - return { - db, - session, ...opts, }; }; diff --git a/src/server/auth.ts b/src/server/auth.ts deleted file mode 100644 index c45c02b..0000000 --- a/src/server/auth.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { - getServerSession, - type DefaultSession, - type NextAuthOptions, -} from "next-auth"; -import GoogleProvider from "next-auth/providers/google"; - -import { env } from "@/env"; -import { db } from "@/server/db"; -import { Adapter } from "next-auth/adapters"; -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -/** - * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. - * - * @see https://next-auth.js.org/configuration/options - */ -export const authOptions: NextAuthOptions = { - adapter: DrizzleAdapter(db) as Adapter, - callbacks: { - session: async ({ session, token }) => { - if (session?.user) { - session.user.id = token.sub ?? ""; - } - return session; - }, - jwt: async ({ user, token }) => { - if (user) { - token.uid = user.id; - } - return token; - }, - }, - session: { - strategy: "jwt", - }, - providers: [ - GoogleProvider({ - clientId: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, - }), - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 460cb36..5e0e35a 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,11 +1,31 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; -import * as schema from "./schema"; -import { env } from "@/env"; +import { env } from "@/env.mjs"; +import { + users, + accounts, + sessions, + verificationTokens, + children, + devices, + pings, + childrenRelations, + deviceRelations, + pingRelations, +} from "@/server/db/schema/_root"; -const client = postgres(env.DATABASE_URL); -export const db = drizzle(client, { schema }); - -// console.log('DRIZZLE', 'migrating'); -// migrate(db, { migrationsFolder: 'drizzle' }) -// .then(() => console.log('DRIZZLE', 'migrated')); +export const client = postgres(env.DATABASE_URL); +export const db = drizzle(client, { + schema: { + accounts, + sessions, + users, + children, + devices, + pings, + verificationTokens, + childrenRelations, + deviceRelations, + pingRelations, + }, +}); diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts new file mode 100644 index 0000000..b9afa86 --- /dev/null +++ b/src/server/db/migrate.ts @@ -0,0 +1,33 @@ +import { env } from "@/env.mjs"; + +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + +const runMigrate = async () => { + if (!env.DATABASE_URL) { + throw new Error("DATABASE_URL is not defined"); + } + + const connection = postgres(env.DATABASE_URL, { max: 1 }); + + const db = drizzle(connection); + + console.log("⏳ Running migrations..."); + + const start = Date.now(); + + await migrate(db, { migrationsFolder: "src/server/db/migrations" }); + + const end = Date.now(); + + console.log("βœ… Migrations completed in", end - start, "ms"); + + process.exit(0); +}; + +runMigrate().catch((err) => { + console.error("❌ Migration failed"); + console.error(err); + process.exit(1); +}); diff --git a/drizzle/0000_wide_gravity.sql b/src/server/db/migrations/0000_slim_katie_power.sql similarity index 50% rename from drizzle/0000_wide_gravity.sql rename to src/server/db/migrations/0000_slim_katie_power.sql index e014db1..1b953c2 100644 --- a/drizzle/0000_wide_gravity.sql +++ b/src/server/db/migrations/0000_slim_katie_power.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS "account" ( - "userId" uuid NOT NULL, + "userId" text NOT NULL, "type" text NOT NULL, "provider" text NOT NULL, "providerAccountId" text NOT NULL, @@ -13,60 +13,33 @@ CREATE TABLE IF NOT EXISTS "account" ( CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId") ); --> statement-breakpoint -CREATE TABLE IF NOT EXISTS "child" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid - () NOT NULL, +CREATE TABLE IF NOT EXISTS "children" ( + "id" varchar(191) PRIMARY KEY NOT NULL, "name" varchar(256) NOT NULL, - "email" varchar(256), - "phone" varchar(256), - "avatar" varchar(256), - "key" varchar(256), - "parent_id" uuid NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "device" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid - () NOT NULL, - "device_id" varchar NOT NULL, - "child_id" uuid NOT NULL, - "device_name" varchar NOT NULL, - "api_key" varchar NOT NULL, - "pin" integer NOT NULL, - "expires" timestamp DEFAULT now - () + interval '1 hour', - CONSTRAINT "device_device_id_unique" UNIQUE("device_id"), - CONSTRAINT "device_api_key_unique" UNIQUE("api_key") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "ping" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid - () NOT NULL, - "device_id" uuid NOT NULL, - "latitude" double precision NOT NULL, - "longitude" double precision NOT NULL, - "timestamp" timestamp NOT NULL + "email" varchar(256) NOT NULL, + "avatar" varchar(256) NOT NULL, + "user_id" varchar(256) NOT NULL ); --> statement-breakpoint CREATE TABLE IF NOT EXISTS "session" ( "sessionToken" text PRIMARY KEY NOT NULL, - "userId" uuid NOT NULL, + "userId" text NOT NULL, "expires" timestamp NOT NULL ); --> statement-breakpoint CREATE TABLE IF NOT EXISTS "user" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid - () NOT NULL, + "id" text PRIMARY KEY NOT NULL, "name" text, "email" text NOT NULL, "emailVerified" timestamp, "image" text ); --> statement-breakpoint -CREATE TABLE IF NOT EXISTS "verification_token" ( - "identifier" varchar(255) NOT NULL, - "token" varchar(255) NOT NULL, +CREATE TABLE IF NOT EXISTS "verificationToken" ( + "identifier" text NOT NULL, + "token" text NOT NULL, "expires" timestamp NOT NULL, - CONSTRAINT "verification_token_identifier_token_pk" PRIMARY KEY("identifier","token") + CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token") ); --> statement-breakpoint DO $$ BEGIN @@ -75,6 +48,12 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; --> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "children" ADD CONSTRAINT "children_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint DO $$ BEGIN ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION diff --git a/src/server/db/migrations/0001_sticky_frightful_four.sql b/src/server/db/migrations/0001_sticky_frightful_four.sql new file mode 100644 index 0000000..75904a9 --- /dev/null +++ b/src/server/db/migrations/0001_sticky_frightful_four.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS "devices" ( + "id" varchar(191) PRIMARY KEY NOT NULL, + "name" varchar(256) NOT NULL, + "device_id" varchar(256) NOT NULL, + "child_id" varchar(256) NOT NULL, + "user_id" varchar(256) NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "devices" ADD CONSTRAINT "devices_child_id_children_id_fk" FOREIGN KEY ("child_id") REFERENCES "children"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "devices" ADD CONSTRAINT "devices_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/0002_abandoned_luckman.sql b/src/server/db/migrations/0002_abandoned_luckman.sql new file mode 100644 index 0000000..cdf9553 --- /dev/null +++ b/src/server/db/migrations/0002_abandoned_luckman.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS "pings" ( + "id" varchar(191) PRIMARY KEY NOT NULL, + "latitude" real NOT NULL, + "longitude" real NOT NULL, + "timestamp" timestamp NOT NULL, + "device_id" varchar(256) NOT NULL, + "user_id" varchar(256) NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pings" ADD CONSTRAINT "pings_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "devices"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "pings" ADD CONSTRAINT "pings_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/server/db/migrations/meta/0000_snapshot.json b/src/server/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..f09fad2 --- /dev/null +++ b/src/server/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,285 @@ +{ + "id": "76648d7a-0659-405a-92c4-4f0763229888", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "5", + "dialect": "pg", + "tables": { + "account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "children": { + "name": "children", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "children_user_id_user_id_fk": { + "name": "children_user_id_user_id_fk", + "tableFrom": "children", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/server/db/migrations/meta/0001_snapshot.json b/src/server/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..71ea5fb --- /dev/null +++ b/src/server/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,352 @@ +{ + "id": "3520a784-4841-418f-8cf4-7e58249c843f", + "prevId": "76648d7a-0659-405a-92c4-4f0763229888", + "version": "5", + "dialect": "pg", + "tables": { + "account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "children": { + "name": "children", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "children_user_id_user_id_fk": { + "name": "children_user_id_user_id_fk", + "tableFrom": "children", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(191)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "child_id": { + "name": "child_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "devices_child_id_children_id_fk": { + "name": "devices_child_id_children_id_fk", + "tableFrom": "devices", + "tableTo": "children", + "columnsFrom": [ + "child_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "devices_user_id_user_id_fk": { + "name": "devices_user_id_user_id_fk", + "tableFrom": "devices", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/src/server/db/migrations/meta/0002_snapshot.json similarity index 70% rename from drizzle/meta/0000_snapshot.json rename to src/server/db/migrations/meta/0002_snapshot.json index 53ff92e..d972530 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/src/server/db/migrations/meta/0002_snapshot.json @@ -1,6 +1,6 @@ { - "id": "c7d09662-b27f-4ac9-b546-1ef12bc6800a", - "prevId": "00000000-0000-0000-0000-000000000000", + "id": "34d0a88b-b9f0-4d72-a427-5f9a72104706", + "prevId": "3520a784-4841-418f-8cf4-7e58249c843f", "version": "5", "dialect": "pg", "tables": { @@ -10,7 +10,7 @@ "columns": { "userId": { "name": "userId", - "type": "uuid", + "type": "text", "primaryKey": false, "notNull": true }, @@ -102,16 +102,15 @@ }, "uniqueConstraints": {} }, - "child": { - "name": "child", + "children": { + "name": "children", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", + "type": "varchar(191)", "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid\n ()" + "notNull": true }, "name": { "name": "name", @@ -123,133 +122,126 @@ "name": "email", "type": "varchar(256)", "primaryKey": false, - "notNull": false - }, - "phone": { - "name": "phone", - "type": "varchar(256)", - "primaryKey": false, - "notNull": false + "notNull": true }, "avatar": { "name": "avatar", "type": "varchar(256)", "primaryKey": false, - "notNull": false + "notNull": true }, - "key": { - "name": "key", + "user_id": { + "name": "user_id", "type": "varchar(256)", "primaryKey": false, - "notNull": false - }, - "parent_id": { - "name": "parent_id", - "type": "uuid", - "primaryKey": false, "notNull": true } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "children_user_id_user_id_fk": { + "name": "children_user_id_user_id_fk", + "tableFrom": "children", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "device": { - "name": "device", + "devices": { + "name": "devices", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", + "type": "varchar(191)", "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid\n ()" + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true }, "device_id": { "name": "device_id", - "type": "varchar", + "type": "varchar(256)", "primaryKey": false, "notNull": true }, "child_id": { "name": "child_id", - "type": "uuid", + "type": "varchar(256)", "primaryKey": false, "notNull": true }, - "device_name": { - "name": "device_name", - "type": "varchar", + "user_id": { + "name": "user_id", + "type": "varchar(256)", "primaryKey": false, "notNull": true - }, - "api_key": { - "name": "api_key", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "pin": { - "name": "pin", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "expires": { - "name": "expires", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now\n () + interval '1 hour'" } }, "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "device_device_id_unique": { - "name": "device_device_id_unique", - "nullsNotDistinct": false, - "columns": [ - "device_id" - ] + "foreignKeys": { + "devices_child_id_children_id_fk": { + "name": "devices_child_id_children_id_fk", + "tableFrom": "devices", + "tableTo": "children", + "columnsFrom": [ + "child_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" }, - "device_api_key_unique": { - "name": "device_api_key_unique", - "nullsNotDistinct": false, - "columns": [ - "api_key" - ] + "devices_user_id_user_id_fk": { + "name": "devices_user_id_user_id_fk", + "tableFrom": "devices", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } - } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} }, - "ping": { - "name": "ping", + "pings": { + "name": "pings", "schema": "", "columns": { "id": { "name": "id", - "type": "uuid", + "type": "varchar(191)", "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid\n ()" - }, - "device_id": { - "name": "device_id", - "type": "uuid", - "primaryKey": false, "notNull": true }, "latitude": { "name": "latitude", - "type": "double precision", + "type": "real", "primaryKey": false, "notNull": true }, "longitude": { "name": "longitude", - "type": "double precision", + "type": "real", "primaryKey": false, "notNull": true }, @@ -258,10 +250,49 @@ "type": "timestamp", "primaryKey": false, "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "pings_device_id_devices_id_fk": { + "name": "pings_device_id_devices_id_fk", + "tableFrom": "pings", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pings_user_id_user_id_fk": { + "name": "pings_user_id_user_id_fk", + "tableFrom": "pings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {} }, @@ -277,7 +308,7 @@ }, "userId": { "name": "userId", - "type": "uuid", + "type": "text", "primaryKey": false, "notNull": true }, @@ -313,10 +344,9 @@ "columns": { "id": { "name": "id", - "type": "uuid", + "type": "text", "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid\n ()" + "notNull": true }, "name": { "name": "name", @@ -348,19 +378,19 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, - "verification_token": { - "name": "verification_token", + "verificationToken": { + "name": "verificationToken", "schema": "", "columns": { "identifier": { "name": "identifier", - "type": "varchar(255)", + "type": "text", "primaryKey": false, "notNull": true }, "token": { "name": "token", - "type": "varchar(255)", + "type": "text", "primaryKey": false, "notNull": true }, @@ -374,8 +404,8 @@ "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": { - "verification_token_identifier_token_pk": { - "name": "verification_token_identifier_token_pk", + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", "columns": [ "identifier", "token" diff --git a/src/server/db/migrations/meta/_journal.json b/src/server/db/migrations/meta/_journal.json new file mode 100644 index 0000000..3207b0e --- /dev/null +++ b/src/server/db/migrations/meta/_journal.json @@ -0,0 +1,27 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1705877584114, + "tag": "0000_slim_katie_power", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1705877897654, + "tag": "0001_sticky_frightful_four", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1706464847221, + "tag": "0002_abandoned_luckman", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts deleted file mode 100644 index 7cf7a52..0000000 --- a/src/server/db/schema.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - timestamp, - text, - primaryKey, - integer, - pgTable, - uuid, - varchar, - pgSchema, - doublePrecision, -} from 'drizzle-orm/pg-core'; -import type { AdapterAccount } from '@auth/core/adapters'; -import { relations, sql } from 'drizzle-orm'; - -//#region auth -//TODO: use this schema once https://github.com/drizzle-team/drizzle-orm/issues/636 is fixed -const authSchema = pgSchema('auth'); -export const users = pgTable('user', { - id: uuid('id') - .notNull() - .primaryKey() - .default(sql`gen_random_uuid - ()`), - name: text('name'), - email: text('email').notNull(), - emailVerified: timestamp('emailVerified', { mode: 'date' }), - image: text('image'), -}); - -export const accounts = pgTable( - 'account', - { - userId: uuid('userId') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - type: text('type').$type().notNull(), - provider: text('provider').notNull(), - providerAccountId: text('providerAccountId').notNull(), - refresh_token: text('refresh_token'), - access_token: text('access_token'), - expires_at: integer('expires_at'), - token_type: text('token_type'), - scope: text('scope'), - id_token: text('id_token'), - session_state: text('session_state'), - }, - (account) => ({ - compoundKey: primaryKey(account.provider, account.providerAccountId), - }), -); - -export const sessions = pgTable('session', { - sessionToken: text('sessionToken').notNull().primaryKey(), - userId: uuid('userId') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - expires: timestamp('expires', { mode: 'date' }).notNull(), -}); - -export const verificationTokens = pgTable( - 'verification_token', - { - identifier: varchar('identifier', { length: 255 }).notNull(), - token: varchar('token', { length: 255 }).notNull(), - expires: timestamp('expires', { mode: 'date' }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey(vt.identifier, vt.token), - }), -); -//#endregion auth - -export const userRelations = relations(users, ({ many }) => ({ - children: many(child), -})); - -//#region child -export const child = pgTable('child', { - id: uuid('id') - .primaryKey() - .default(sql`gen_random_uuid - ()`), - name: varchar('name', { length: 256 }).notNull(), - email: varchar('email', { length: 256 }), - phone: varchar('phone', { length: 256 }), - avatar: varchar('avatar', { length: 256 }), - apiKey: varchar('key', { length: 256 }), - parentId: uuid('parent_id').notNull(), -}); -export const childRelations = relations(child, ({ one, many }) => ({ - parent: one(users, { - fields: [child.parentId], - references: [users.id], - }), - devices: many(device), -})); - -export const device = pgTable('device', { - id: uuid('id') - .primaryKey() - .default(sql`gen_random_uuid - ()`), - deviceId: varchar('device_id').notNull().unique(), - childId: uuid('child_id').notNull(), - deviceName: varchar('device_name').notNull(), - apiKey: varchar('api_key').notNull().unique(), - //TODO: make the device request/pin a separate table and enforce the expiry - pin: integer('pin').notNull(), - expires: timestamp('expires').default(sql`now - () + interval '1 hour'`), -}); -export const deviceRelations = relations(device, ({ one, many }) => ({ - child: one(child, { - fields: [device.childId], - references: [child.id], - }), - pings: many(ping), -})); - -export const ping = pgTable('ping', { - id: uuid('id') - .primaryKey() - .default(sql`gen_random_uuid - ()`), - deviceId: uuid('device_id').notNull(), - latitude: doublePrecision('latitude').notNull(), - longitude: doublePrecision('longitude').notNull(), - timestamp: timestamp('timestamp').notNull(), -}); -export const pingRelations = relations(ping, ({ one, many }) => ({ - device: one(device, { - fields: [ping.deviceId], - references: [device.id], - }), -})); diff --git a/src/server/db/schema/_root.ts b/src/server/db/schema/_root.ts new file mode 100644 index 0000000..ac7d07b --- /dev/null +++ b/src/server/db/schema/_root.ts @@ -0,0 +1,16 @@ +import { users, accounts, sessions, verificationTokens } from "./auth"; +import { children, childrenRelations } from "./children"; +import { deviceRelations, devices } from "./devices"; +import { pingRelations, pings } from "./pings"; +export { + pings, + devices, + children, + users, + accounts, + sessions, + verificationTokens, + childrenRelations, + deviceRelations, + pingRelations, +}; diff --git a/src/server/db/schema/auth.ts b/src/server/db/schema/auth.ts new file mode 100644 index 0000000..f041d0a --- /dev/null +++ b/src/server/db/schema/auth.ts @@ -0,0 +1,58 @@ +import { + timestamp, + pgTable, + text, + primaryKey, + integer, +} from "drizzle-orm/pg-core"; +import type { AdapterAccount } from "@auth/core/adapters"; + +export const users = pgTable("user", { + id: text("id").notNull().primaryKey(), + name: text("name"), + email: text("email").notNull(), + emailVerified: timestamp("emailVerified", { mode: "date" }), + image: text("image"), +}); + +export const accounts = pgTable( + "account", + { + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("providerAccountId").notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), + }, + (account) => ({ + compoundKey: primaryKey(account.provider, account.providerAccountId), + }) +); + +export const sessions = pgTable("session", { + sessionToken: text("sessionToken").notNull().primaryKey(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: timestamp("expires", { mode: "date" }).notNull(), +}); + +export const verificationTokens = pgTable( + "verificationToken", + { + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey(vt.identifier, vt.token), + }) +); diff --git a/src/server/db/schema/children.ts b/src/server/db/schema/children.ts new file mode 100644 index 0000000..f63d608 --- /dev/null +++ b/src/server/db/schema/children.ts @@ -0,0 +1,53 @@ +import { varchar, pgTable } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { type z } from "zod"; + +import { users } from "@/server/db/schema/auth"; +import { type getChildren } from "@/lib/api/children/queries"; + +import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; +import { devices } from "./devices"; + +export const children = pgTable("children", { + id: varchar("id", { length: 191 }) + .primaryKey() + .$defaultFn(() => randomUUID()), + name: varchar("name", { length: 256 }).notNull(), + email: varchar("email", { length: 256 }).notNull(), + avatar: varchar("avatar", { length: 256 }).notNull(), + userId: varchar("user_id", { length: 256 }) + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), +}); +export const childrenRelations = relations(children, ({ many }) => ({ + devices: many(devices), +})); + +// Schema for children - used to validate API requests +export const insertChildSchema = createInsertSchema(children); + +export const insertChildParams = createSelectSchema(children, {}).omit({ + id: true, + userId: true, +}); + +export const updateChildSchema = createSelectSchema(children); + +export const updateChildParams = createSelectSchema(children, {}).omit({ + userId: true, +}); + +export const childIdSchema = updateChildSchema.pick({ id: true }); + +// Types for children - used to type API request params and within Components +export type Child = z.infer; +export type NewChild = z.infer; +export type NewChildParams = z.infer; +export type UpdateChildParams = z.infer; +export type ChildId = z.infer["id"]; + +// this type infers the return from getChildren() - meaning it will include any joins +export type CompleteChild = Awaited< + ReturnType +>["children"][number]; diff --git a/src/server/db/schema/devices.ts b/src/server/db/schema/devices.ts new file mode 100644 index 0000000..6dbd62e --- /dev/null +++ b/src/server/db/schema/devices.ts @@ -0,0 +1,68 @@ +import { + varchar, + pgTable, + PgColumn, + PgTableWithColumns, +} from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { children } from "./children"; +import { users } from "@/server/db/schema/auth"; +import { type getDevices } from "@/lib/api/devices/queries"; + +import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; +import { pings } from "./pings"; + +export const devices = pgTable("devices", { + id: varchar("id", { length: 191 }) + .primaryKey() + .$defaultFn(() => randomUUID()), + name: varchar("name", { length: 256 }).notNull(), + deviceId: varchar("device_id", { length: 256 }).notNull(), + childId: varchar("child_id", { length: 256 }) + .references(() => children.id, { onDelete: "cascade" }) + .notNull(), + userId: varchar("user_id", { length: 256 }) + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), +}); +export const deviceRelations = relations(devices, ({ one, many }) => ({ + pings: many(pings), + child: one(children, { + fields: [devices.childId], + references: [children.id], + }), +})); + +// Schema for devices - used to validate API requests +export const insertDeviceSchema = createInsertSchema(devices); + +export const insertDeviceParams = createSelectSchema(devices, { + childId: z.coerce.string().min(1), +}).omit({ + id: true, + userId: true, +}); + +export const updateDeviceSchema = createSelectSchema(devices); + +export const updateDeviceParams = createSelectSchema(devices, { + childId: z.coerce.string().min(1), +}).omit({ + userId: true, +}); + +export const deviceIdSchema = updateDeviceSchema.pick({ id: true }); + +// Types for devices - used to type API request params and within Components +export type Device = z.infer; +export type NewDevice = z.infer; +export type NewDeviceParams = z.infer; +export type UpdateDeviceParams = z.infer; +export type DeviceId = z.infer["id"]; + +// this type infers the return from getDevices() - meaning it will include any joins +export type CompleteDevice = Awaited< + ReturnType +>["devices"][number]; diff --git a/src/server/db/schema/pings.ts b/src/server/db/schema/pings.ts new file mode 100644 index 0000000..acde260 --- /dev/null +++ b/src/server/db/schema/pings.ts @@ -0,0 +1,67 @@ +import { real, timestamp, varchar, pgTable } from "drizzle-orm/pg-core"; +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { devices } from "./devices"; +import { users } from "@/server/db/schema/auth"; +import { type getPings } from "@/lib/api/pings/queries"; + +import { randomUUID } from "crypto"; +import { relations } from "drizzle-orm"; + +export const pings = pgTable("pings", { + id: varchar("id", { length: 191 }) + .primaryKey() + .$defaultFn(() => randomUUID()), + latitude: real("latitude").notNull(), + longitude: real("longitude").notNull(), + timestamp: timestamp("timestamp").notNull(), + deviceId: varchar("device_id", { length: 256 }) + .references(() => devices.id, { onDelete: "cascade" }) + .notNull(), + userId: varchar("user_id", { length: 256 }) + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), +}); +export const pingRelations = relations(pings, ({ one }) => ({ + device: one(devices, { + fields: [pings.deviceId], + references: [devices.id], + }), +})); +// Schema for pings - used to validate API requests +export const insertPingSchema = createInsertSchema(pings); + +export const insertPingParams = createSelectSchema(pings, { + latitude: z.coerce.number(), + longitude: z.coerce.number(), + timestamp: z.coerce.string().min(1), + deviceId: z.coerce.string().min(1), +}).omit({ + id: true, + userId: true, +}); + +export const updatePingSchema = createSelectSchema(pings); + +export const updatePingParams = createSelectSchema(pings, { + latitude: z.coerce.number(), + longitude: z.coerce.number(), + timestamp: z.coerce.string().min(1), + deviceId: z.coerce.string().min(1), +}).omit({ + userId: true, +}); + +export const pingIdSchema = updatePingSchema.pick({ id: true }); + +// Types for pings - used to type API request params and within Components +export type Ping = z.infer; +export type NewPing = z.infer; +export type NewPingParams = z.infer; +export type UpdatePingParams = z.infer; +export type PingId = z.infer["id"]; + +// this type infers the return from getPings() - meaning it will include any joins +export type CompletePing = Awaited< + ReturnType +>["pings"][number]; diff --git a/src/server/db/scripts/auth.ts b/src/server/db/scripts/auth.ts deleted file mode 100644 index 276e6e0..0000000 --- a/src/server/db/scripts/auth.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from ".."; -import * as schema from "../schema"; - -console.log("auth", "Seeding auth"); -await db - .insert(schema.accounts) - .values([ - { - userId: "2250f34e-997a-44de-ab8d-beddeda13525", - provider: "google", - type: "oauth", - providerAccountId: "112561477626832751929", - access_token: "FARTS", - expires_at: 9, - token_type: "Bearer", - scope: - "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", - id_token: "FARTS", - }, - ]) - .execute(); - -console.log("auth", "Seeded auth"); diff --git a/src/server/db/scripts/seed.ts b/src/server/db/scripts/seed.ts deleted file mode 100644 index 62f737e..0000000 --- a/src/server/db/scripts/seed.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ -import { db } from ".."; -import * as schema from "../schema"; -import { faker } from "@faker-js/faker"; - -console.log("seed", "Seeding"); -const seedUsers = [ - { - id: "2250f34e-997a-44de-ab8d-beddeda13525", - name: "Fergal Moran", - email: "fergal.moran@gmail.com", - emailVerified: new Date(), - }, -]; -await db.insert(schema.users).values(seedUsers).execute(); - -const seedChildren = [ - { - id: "2250f34e-997a-44de-ab8d-beddeda13525", - name: "Lil Debuggles", - phone: "123 456 789", - email: "lildebuggles@kidarr.com", - avatar: faker.image.avatar(), - parentId: seedUsers[0]?.id!, - }, -]; -await db.insert(schema.child).values(seedChildren).execute(); - -const seedDevices = [ - { - id: "5af79a30-df27-4646-9d9f-77e19b4191c1", - deviceId: "373791e3-afe3-49de-b0a2-842a44071585", - childId: seedChildren[0]?.id!, - deviceName: "Not an iPhone", - apiKey: "nQhXtqemsWjzBpbDxlIV2qtDx9xxO4oZVBJADdhJLfA=", - pin: 1234, - expires: new Date(2065), - }, -]; -await db.insert(schema.device).values(seedDevices).execute(); - -const seedPings = [ - { - deviceId: seedDevices[0]?.id!, - latitude: 51.903614, - longitude: -8.468399, - timestamp: new Date(), - }, - { - deviceId: seedDevices[0]?.id!, - latitude: 51.8985, - longitude: -8.4756, - timestamp: new Date(), - }, - { - deviceId: seedDevices[0]?.id!, - latitude: 51.93588161110811, - longitude: -8.495129534566756, - timestamp: new Date(), - }, -]; -await db.insert(schema.ping).values(seedPings).execute(); -console.log("seed", "Seeded"); diff --git a/src/server/db/seed/auth.ts b/src/server/db/seed/auth.ts new file mode 100644 index 0000000..7e2f816 --- /dev/null +++ b/src/server/db/seed/auth.ts @@ -0,0 +1,31 @@ +import { db } from ".."; +import { accounts } from "../schema/auth"; + +const runAuthSeed = async () => { + console.log("πŸ”", "Seeding auth"); + await db + .insert(accounts) + .values([ + { + userId: "2250f34e-997a-44de-ab8d-beddeda13525", + provider: "google", + type: "oauth", + providerAccountId: "112561477626832751929", + access_token: "FARTS", + expires_at: 9, + token_type: "Bearer", + scope: + "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", + id_token: "FARTS", + }, + ]) + .execute(); + + console.log("🌱", "Seeded auth"); + process.exit(0); +}; +runAuthSeed().catch((err) => { + console.error("❌ Auth seeding failed"); + console.error(err); + process.exit(1); +}); diff --git a/src/server/db/scripts/seed copy.ts b/src/server/db/seed/seed.ts similarity index 60% rename from src/server/db/scripts/seed copy.ts rename to src/server/db/seed/seed.ts index 88077f7..77fb09b 100644 --- a/src/server/db/scripts/seed copy.ts +++ b/src/server/db/seed/seed.ts @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ import { db } from ".."; -import * as schema from "../schema"; import { faker } from "@faker-js/faker"; +import { users } from "../schema/auth"; +import { children } from "../schema/children"; +import { devices } from "../schema/devices"; +import { pings } from "../schema/pings"; -const main = async () => { - console.log("seed", "Seeding"); +console.log("seed", "Seeding"); +const runSeed = async () => { + console.log("πŸ‘―", "Seeding users"); const seedUsers = [ { id: "2250f34e-997a-44de-ab8d-beddeda13525", @@ -13,7 +17,7 @@ const main = async () => { emailVerified: new Date(), }, ]; - await db.insert(schema.users).values(seedUsers).execute(); + await db.insert(users).values(seedUsers).execute(); const seedChildren = [ { @@ -22,53 +26,55 @@ const main = async () => { phone: "123 456 789", email: "lildebuggles@kidarr.com", avatar: faker.image.avatar(), - parentId: seedUsers[0]?.id!, + userId: seedUsers[0]?.id!, }, ]; - await db.insert(schema.child).values(seedChildren).execute(); + await db.insert(children).values(seedChildren).execute(); const seedDevices = [ { id: "5af79a30-df27-4646-9d9f-77e19b4191c1", + name: "Not an iPhone", deviceId: "373791e3-afe3-49de-b0a2-842a44071585", childId: seedChildren[0]?.id!, + userId: seedUsers[0]?.id!, deviceName: "Not an iPhone", apiKey: "nQhXtqemsWjzBpbDxlIV2qtDx9xxO4oZVBJADdhJLfA=", pin: 1234, expires: new Date(2065), }, ]; - await db.insert(schema.device).values(seedDevices).execute(); + await db.insert(devices).values(seedDevices).execute(); const seedPings = [ { deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, latitude: 51.903614, longitude: -8.468399, timestamp: new Date(), }, { deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, latitude: 51.8985, longitude: -8.4756, timestamp: new Date(), }, { deviceId: seedDevices[0]?.id!, + userId: seedUsers[0]?.id!, latitude: 51.93588161110811, longitude: -8.495129534566756, timestamp: new Date(), }, ]; - await db.insert(schema.ping).values(seedPings).execute(); - return; + await db.insert(pings).values(seedPings).execute(); + console.log("🌱", "Seeded users"); + process.exit(0); }; - -main() - .then(() => { - console.log("seed", "Seeded"); - return; - }) - .catch((err) => { - console.error("seed", "Error seeding", err); - }); +runSeed().catch((err) => { + console.error("❌ Seeding failed"); + console.error(err); + process.exit(1); +}); diff --git a/src/styles/globals.css b/src/styles/globals.css index cb3e8dc..9477a51 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -3,92 +3,83 @@ @tailwind utilities; @layer base { - :root { - --card: 283 36% 97%; - --ring: 283 93% 25%; - --input: 220 13% 91%; - --muted: 283 13% 92%; - --accent: 283 15% 81%; - --border: 220 13% 91%; - --popover: 283 36% 98%; - --primary: 283 93% 25%; - --secondary: 283 7% 90%; - --background: 283 36% 98%; - --foreground: 283 68% 2%; - --destructive: 4 84% 36%; - --card-foreground: 283 68% 1%; - --muted-foreground: 283 4% 37%; - --accent-foreground: 283 15% 21%; - --popover-foreground: 283 68% 2%; - --primary-foreground: 283 93% 85%; - --secondary-foreground: 283 7% 30%; - --destructive-foreground: 4 84% 96%; - --radius: 0.5rem; - } - - .dark { - --card: 283 47% 3%; - --ring: 283 93% 25%; - --input: 215 27.9% 16.9%; - --muted: 283 13% 8%; - --accent: 283 25% 17%; - --border: 215 27.9% 16.9%; - --popover: 283 47% 2%; - --primary: 283 93% 25%; - --secondary: 283 18% 12%; - --background: 283 47% 2%; - --foreground: 283 27% 98%; - --destructive: 4 84% 49%; - --card-foreground: 283 27% 99%; - --muted-foreground: 283 4% 63%; - --accent-foreground: 283 25% 77%; - --popover-foreground: 283 27% 98%; - --primary-foreground: 283 93% 85%; - --secondary-foreground: 283 18% 72%; - --destructive-foreground: 0 0% 100%; - } + :root { + --card: 258 70% 100%; + --ring: 258 58% 37%; + --input: 220 13% 91%; + --muted: 258 29% 85%; + --accent: 258 19% 81%; + --border: 220 13% 91%; + --popover: 258 70% 100%; + --primary: 258 58% 37%; + --secondary: 258 19% 81%; + --background: 258 70% 100%; + --foreground: 258 77% 0%; + --destructive: 19 98% 27%; + --card-foreground: 258 77% 0%; + --muted-foreground: 258 10% 40%; + --accent-foreground: 258 19% 21%; + --popover-foreground: 258 77% 0%; + --primary-foreground: 258 58% 97%; + --secondary-foreground: 258 19% 21%; + --destructive-foreground: 19 98% 87%; + --radius: 0.5rem; } -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } + .dark { + --card: 258 53% 3%; + --ring: 258 58% 37%; + --input: 215 27.9% 16.9%; + --muted: 258 29% 15%; + --accent: 258 15% 10%; + --border: 215 27.9% 16.9%; + --popover: 258 53% 3%; + --primary: 258 58% 37%; + --secondary: 258 15% 10%; + --background: 258 53% 3%; + --foreground: 258 40% 97%; + --destructive: 19 98% 46%; + --card-foreground: 258 40% 97%; + --muted-foreground: 258 10% 60%; + --accent-foreground: 258 15% 70%; + --popover-foreground: 258 40% 97%; + --primary-foreground: 258 58% 97%; + --secondary-foreground: 258 15% 70%; + --destructive-foreground: 0 0% 100%; + } } .map { - width: 100%; - height: 40rem; + width: 100%; + height: 40rem; } img.leaflet-marker-icon { - border-radius: 50%; - border: 2px solid red; + border-radius: 50%; + border: 2px solid red; } .leaflet-control { - z-index: 0 !important; + z-index: 0 !important; } .leaflet-pane { - z-index: 0 !important; + z-index: 0 !important; } .leaflet-top, .leaflet-bottom { - z-index: 0 !important; + z-index: 0 !important; } .leaflet-popup-content-wrapper { - border-radius: 0px; - padding: 0px !important; + border-radius: 0px; + padding: 0px !important; } .leaflet-popup-content { - padding: 0px !important; - border-radius: 0px !important; - margin-right: 0px !important; - margin-left: 0px !important; + padding: 0px !important; + border-radius: 0px !important; + margin-right: 0px !important; + margin-left: 0px !important; } diff --git a/src/trpc/react.tsx b/src/trpc/react.tsx index 9f8d59d..a9b493a 100644 --- a/src/trpc/react.tsx +++ b/src/trpc/react.tsx @@ -10,10 +10,7 @@ import { getUrl, transformer } from "./shared"; export const api = createTRPCReact(); -export function TRPCReactProvider(props: { - children: React.ReactNode; - cookies: string; -}) { +export function TRPCReactProvider(props: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => @@ -27,15 +24,9 @@ export function TRPCReactProvider(props: { }), unstable_httpBatchStreamLink({ url: getUrl(), - headers() { - return { - cookie: props.cookies, - "x-trpc-source": "react", - }; - }, }), ], - }), + }) ); return ( diff --git a/src/trpc/server.ts b/src/trpc/server.ts index fa40ddb..e0ffd12 100644 --- a/src/trpc/server.ts +++ b/src/trpc/server.ts @@ -8,7 +8,7 @@ import { import { callProcedure } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { type TRPCErrorResponse } from "@trpc/server/rpc"; -import { cookies } from "next/headers"; +import { headers } from "next/headers"; import { cache } from "react"; import { appRouter, type AppRouter } from "@/server/api/root"; @@ -20,11 +20,11 @@ import { transformer } from "./shared"; * handling a tRPC call from a React Server Component. */ const createContext = cache(() => { + const heads = new Headers(headers()); + heads.set("x-trpc-source", "rsc"); + return createTRPCContext({ - headers: new Headers({ - cookie: cookies().toString(), - "x-trpc-source": "rsc", - }), + headers: heads, }); }); diff --git a/tailwind.config.ts b/tailwind.config.ts index 84287e8..d79e2ce 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,14 +1,8 @@ -import type { Config } from "tailwindcss" +import type { Config } from "tailwindcss"; const config = { darkMode: ["class"], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - ], - prefix: "", + content: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"], theme: { container: { center: true, @@ -54,10 +48,13 @@ const config = { }, }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", + lg: `var(--radius)`, + md: `calc(var(--radius) - 2px)`, sm: "calc(var(--radius) - 4px)", }, + // fontFamily: { + // sans: ["var(--font-sans)", ...fontFamily.sans], + // }, keyframes: { "accordion-down": { from: { height: "0" }, @@ -75,6 +72,6 @@ const config = { }, }, plugins: [require("tailwindcss-animate")], -} satisfies Config +} satisfies Config; -export default config \ No newline at end of file +export default config; diff --git a/tsconfig.json b/tsconfig.json index c5eef6e..fbd998d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,35 @@ { "compilerOptions": { - /* Base Options: */ "esModuleInterop": true, "skipLibCheck": true, - "target": "es2022", + "target": "esnext", "allowJs": true, "resolveJsonModule": true, "moduleDetection": "force", "isolatedModules": true, - - /* Strictness */ "strict": true, "noUncheckedIndexedAccess": true, "checkJs": true, - - /* Bundled projects */ - "lib": ["dom", "dom.iterable", "ES2022"], + "lib": [ + "dom", + "dom.iterable", + "ES2022" + ], "noEmit": true, "module": "ESNext", "moduleResolution": "Bundler", "jsx": "preserve", - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "incremental": true, - - /* Path Aliases */ - "baseUrl": ".", + "baseUrl": "./", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, "include": [ @@ -38,5 +41,7 @@ "**/*.js", ".next/types/**/*.ts" ], - "exclude": ["node_modules"] -} + "exclude": [ + "node_modules" + ] +} \ No newline at end of file