From 24e26177a57fef547fb23c16d14d8fdb54e216f9 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 16 Oct 2023 23:18:27 +0100 Subject: [PATCH] First thingy doney --- .editorconfig | 14 + .idea/.gitignore | 8 + .idea/codeStyles/Project.xml | 65 ++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/dataSources.xml | 12 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/modules.xml | 8 + .idea/parentgrine-server.iml | 12 + .idea/vcs.xml | 6 + .prettierrc | 7 + .vscode/settings.json | 14 +- bun.lockb | Bin 188632 -> 231131 bytes components.json | 4 +- drizzle.config.ts | 14 + drizzle/0000_thankful_kronos.sql | 60 ++++ drizzle/meta/0000_snapshot.json | 295 ++++++++++++++++++ drizzle/meta/_journal.json | 13 + next.config.js | 6 +- package.json | 34 +- public/site.webmanifest | 2 +- scripts/reset.sh | 15 + src/app/(auth)/signin/page.tsx | 47 +++ src/app/(dashboard)/dashboard/page.tsx | 19 ++ src/app/(dashboard)/layout.tsx | 15 + src/app/api/auth/[...nextauth]/route.ts | 5 + src/app/api/child/route.ts | 18 ++ src/app/globals.css | 106 +++---- src/app/layout.tsx | 49 ++- src/app/page.tsx | 108 ++++++- .../children/add-child-component.tsx | 58 ++++ src/components/children/child-select-list.tsx | 50 +++ src/components/children/children-filter.tsx | 16 + src/components/forms/add-child-form.tsx | 132 ++++++++ src/components/forms/user-auth-form.tsx | 128 ++++++++ src/components/header/auth-header.tsx | 77 +++++ src/components/header/site-header.tsx | 58 ++++ src/components/header/theme-toggle.tsx | 23 ++ src/components/icons.tsx | 68 ++++ src/components/main-nav.tsx | 41 +++ src/components/maps/main-map.tsx | 14 +- .../providers/tanstack-provider.tsx | 15 + src/components/theme-provider.tsx | 9 + src/components/user-avatar.tsx | 24 ++ src/config/site.ts | 20 ++ src/db/schema/auth.ts | 59 ++++ src/db/schema/children.ts | 34 ++ src/db/schema/index.ts | 7 + src/lib/models/child.ts | 4 + src/lib/models/location.ts | 4 + src/lib/services/auth/config.ts | 16 + src/lib/services/auth/provider.tsx | 12 + src/lib/validations/auth.ts | 5 + src/lib/validations/child.ts | 5 + src/types/nav.ts | 6 + tsconfig.json | 10 +- 55 files changed, 1755 insertions(+), 107 deletions(-) create mode 100644 .editorconfig create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/parentgrine-server.iml create mode 100644 .idea/vcs.xml create mode 100644 .prettierrc create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_thankful_kronos.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100755 scripts/reset.sh create mode 100644 src/app/(auth)/signin/page.tsx create mode 100644 src/app/(dashboard)/dashboard/page.tsx create mode 100644 src/app/(dashboard)/layout.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/child/route.ts create mode 100644 src/components/children/add-child-component.tsx create mode 100644 src/components/children/child-select-list.tsx create mode 100644 src/components/children/children-filter.tsx create mode 100644 src/components/forms/add-child-form.tsx create mode 100644 src/components/forms/user-auth-form.tsx create mode 100644 src/components/header/auth-header.tsx create mode 100644 src/components/header/site-header.tsx create mode 100644 src/components/header/theme-toggle.tsx create mode 100644 src/components/icons.tsx create mode 100644 src/components/main-nav.tsx create mode 100644 src/components/providers/tanstack-provider.tsx create mode 100644 src/components/theme-provider.tsx create mode 100644 src/components/user-avatar.tsx create mode 100644 src/config/site.ts create mode 100644 src/db/schema/auth.ts create mode 100644 src/db/schema/children.ts create mode 100644 src/db/schema/index.ts create mode 100644 src/lib/models/child.ts create mode 100644 src/lib/models/location.ts create mode 100644 src/lib/services/auth/config.ts create mode 100644 src/lib/services/auth/provider.tsx create mode 100644 src/lib/validations/auth.ts create mode 100644 src/lib/validations/child.ts create mode 100644 src/types/nav.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1022108 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 80 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..617b9df --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..230c6e0 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/parentgrine + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a862b72 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/parentgrine-server.iml b/.idea/parentgrine-server.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/parentgrine-server.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..943f43d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "singleAttributePerLine": true +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 9f16be9..b5308ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,18 @@ { "editor.fontFamily": "'Hack Nerd Font Mono', 'monospace', monospace", - "workbench.colorTheme": ".NET Purple theme dark", + "files.exclude": { + "node_modules": true, + ".idea": true, + ".vscode": true, + ".next": true, + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "workbench.colorTheme": "Pink Cat Boo", "workbench.iconTheme": "vscode-icontheme-nomo-dark-macos", "workbench.colorCustomizations": { "activityBar.activeBackground": "#ce0c36", diff --git a/bun.lockb b/bun.lockb index fd86a49f20238e8ed0434d07c9c0ca844b7defdd..b8b8540a67ad6df96be92c1867bcc0c3c4d6e404 100755 GIT binary patch delta 76285 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 QPlj~J-{^Rm+gyW&QyJ*l7M(u_ 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 WfL95pt?#CnXrgSbKBdG?%l`ujNKTOe diff --git a/components.json b/components.json index af6ba6d..b050396 100644 --- a/components.json +++ b/components.json @@ -6,11 +6,11 @@ "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", - "baseColor": "neutral", + "baseColor": "zinc", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..43d26a9 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,14 @@ +import type { Config } from "drizzle-kit"; +import 'dotenv/config' + +if (!process.env.DATABASE_URL) { + throw new Error("DATABASE_URL is missing"); +} +export default { + schema: "./src/db/schema/*", + out: "./drizzle", + driver: "pg", + dbCredentials: { + connectionString: process.env.DATABASE_URL as string, + }, +} satisfies Config; diff --git a/drizzle/0000_thankful_kronos.sql b/drizzle/0000_thankful_kronos.sql new file mode 100644 index 0000000..81fc16b --- /dev/null +++ b/drizzle/0000_thankful_kronos.sql @@ -0,0 +1,60 @@ +CREATE TABLE IF NOT EXISTS "account" ( + "userId" text NOT NULL, + "type" text NOT NULL, + "provider" text NOT NULL, + "providerAccountId" text NOT NULL, + "refresh_token" text, + "access_token" text, + "expires_at" integer, + "token_type" text, + "scope" text, + "id_token" text, + "session_state" text, + CONSTRAINT account_provider_providerAccountId PRIMARY KEY("provider","providerAccountId") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "session" ( + "sessionToken" text PRIMARY KEY NOT NULL, + "userId" text NOT NULL, + "expires" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text, + "email" text NOT NULL, + "emailVerified" timestamp, + "image" text +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "verificationToken" ( + "identifier" text NOT NULL, + "token" text NOT NULL, + "expires" timestamp NOT NULL, + CONSTRAINT verificationToken_identifier_token PRIMARY KEY("identifier","token") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "children" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar(256), + "phone" varchar(256) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "locations" ( + "uuid1" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "longitude" numeric, + "latitude" numeric, + "user_id" uuid +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") 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 + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..13f4dd3 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,295 @@ +{ + "version": "5", + "dialect": "pg", + "id": "458faab0-c0e4-4810-be47-6cce7e094f0f", + "prevId": "00000000-0000-0000-0000-000000000000", + "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": { + "name": "account_provider_providerAccountId", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "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": { + "name": "verificationToken_identifier_token", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + }, + "children": { + "name": "children", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "locations": { + "name": "locations", + "schema": "", + "columns": { + "uuid1": { + "name": "uuid1", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "longitude": { + "name": "longitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..5d6a071 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1697494100554, + "tag": "0000_thankful_kronos", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 767719f..662cb89 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + env: {}, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/package.json b/package.json index 07d2549..9f92a0c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { - "name": "parentgrin", + "name": "parentgrine", "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev2": "next dev", + "dev": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --key /etc/letsencrypt/live/dev.fergl.ie/privkey.pem --cert /etc/letsencrypt/live/dev.fergl.ie/fullchain.pem --source 3000 --target 3002", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { - "@hookform/resolvers": "^3.3.1", + "@auth/drizzle-adapter": "^0.3.3", + "@hookform/resolvers": "^3.3.2", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", @@ -36,15 +38,25 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", + "axios": "^1.5.1", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", "date-fns": "^2.30.0", + "dotenv": "^16.3.1", + "drizzle-orm": "^0.28.6", + "http-status-codes": "^2.3.0", "leaflet": "^1.9.4", - "lucide-react": "^0.284.0", + "local-ssl-proxy": "^2.0.5", + "lucide-react": "^0.287.0", "next": "13.5.4", + "next-auth": "^4.23.2", + "next-themes": "^0.2.1", + "postgres": "^3.4.0", "react": "^18", - "react-day-picker": "^8.8.2", + "react-day-picker": "^8.9.0", "react-dom": "^18", "react-hook-form": "^7.47.0", "react-leaflet": "^4.2.1", @@ -53,15 +65,17 @@ "zod": "^3.22.4" }, "devDependencies": { - "typescript": "^5", + "@types/leaflet": "^1.9.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "@types/leaflet": "^1.9.6", "autoprefixer": "^10", - "postcss": "^8", - "tailwindcss": "^3", + "drizzle-kit": "^0.19.13", "eslint": "^8", - "eslint-config-next": "13.5.4" + "eslint-config-next": "13.5.4", + "postcss": "^8", + "prettier": "3.0.3", + "tailwindcss": "^3", + "typescript": "^5" } } diff --git a/public/site.webmanifest b/public/site.webmanifest index 5e2a42a..4704116 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,6 +1,6 @@ { "name": "Parent Grin", - "short_name": "parentgrin", + "short_name": "parentgrine", "icons": [ { "src": "/android-chrome-192x192.png", diff --git a/scripts/reset.sh b/scripts/reset.sh new file mode 100755 index 0000000..63518cf --- /dev/null +++ b/scripts/reset.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +export PGUSER=postgres +export PGPASSWORD=hackme +export PGHOST=localhost + +echo Removing migrations +rm -rf drizzle +echo "Dropping db" + +dropdb -f --if-exists parentgrine +echo "Creating db" +createdb parentgrine + +bunx drizzle-kit generate:pg --config=./drizzle.config.ts +bunx drizzle-kit push:pg --config=./drizzle.config.ts diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx new file mode 100644 index 0000000..450e694 --- /dev/null +++ b/src/app/(auth)/signin/page.tsx @@ -0,0 +1,47 @@ +import { UserAuthForm } from '@/components/forms/add-child-form'; +import { Icons } from '@/components/icons'; +import { buttonVariants } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import Link from 'next/link'; +import React from 'react'; + +const SigninPage = () => { + return ( +

+ + <> + + Back + + +
+
+ +

+ Welcome back +

+

+ Enter your email to sign in to your account +

+
+ +

+ + Don't have an account? Sign Up + +

+
+
+ ); +}; + +export default SigninPage; diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..5018bf2 --- /dev/null +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import MainMap from '@/components/maps/main-map' +import ChildrenFilter from '@/components/children/children-filter' + +const DashboardPage = async () => { + return ( +
+
+ +
+
This is the dashboard
+
+ +
+
+ ) +} + +export default DashboardPage diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..98dff15 --- /dev/null +++ b/src/app/(dashboard)/layout.tsx @@ -0,0 +1,15 @@ +import { SiteHeader } from '@/components/header/site-header' +import React from 'react' + +type DashboardLayoutProps = { + children?: React.ReactNode +} +const DashboardLayout = async ({ children }: DashboardLayoutProps) => { + return ( +
+ +
{children}
+
+ ) +} +export default DashboardLayout diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..ef714f0 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,5 @@ +import NextAuth from 'next-auth' +import authOptions from '@/lib/services/auth/config' + +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST } diff --git a/src/app/api/child/route.ts b/src/app/api/child/route.ts new file mode 100644 index 0000000..39c30e4 --- /dev/null +++ b/src/app/api/child/route.ts @@ -0,0 +1,18 @@ +import db from '@/db/schema'; +import { children } from '@/db/schema/children'; +import { getServerSession } from 'next-auth'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { NextResponse } from 'next/server'; +import authOptions from '@/lib/services/auth/config'; + +export async function GET(request: Request) { + const session = await getServerSession(authOptions); + if (!session) + return NextResponse.json( + { error: getReasonPhrase(StatusCodes.UNAUTHORIZED) }, + { status: StatusCodes.UNAUTHORIZED } + ); + const activeChildren = await db.select().from(children); + + return NextResponse.json(activeChildren); +} diff --git a/src/app/globals.css b/src/app/globals.css index ffe3c7e..e765625 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,73 +5,47 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - + --foreground: 20 14.3% 4.1%; --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - + --card-foreground: 20 14.3% 4.1%; --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - + --popover-foreground: 20 14.3% 4.1%; + --primary: 24.6 95% 53.1%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - - --radius: 0.5rem; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 24.6 95% 53.1%; + --radius: 0.75rem; } .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 20.5 90.2% 48.2%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 20.5 90.2% 48.2%; } } @@ -79,3 +53,13 @@ width: 100%; height: 40rem; } +.leaflet-control { + z-index: 0 !important; +} +.leaflet-pane { + z-index: 0 !important; +} +.leaflet-top, +.leaflet-bottom { + z-index: 0 !important; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7f94904..49d44c4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,17 +1,23 @@ -import "./globals.css"; -import "leaflet/dist/leaflet.css"; +import React from 'react'; -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import './globals.css'; +import 'leaflet/dist/leaflet.css'; -const inter = Inter({ subsets: ["latin"] }); +import type { Metadata } from 'next'; +import { Sanchez } from 'next/font/google'; +import NextAuthProvider from '@/lib/services/auth/provider'; +import { ThemeProvider } from '@/components/theme-provider'; +import { cn } from '@/lib/utils'; +import TanstackProvider from '@/components/providers/tanstack-provider'; + +const font = Sanchez({ subsets: ['latin'], weight: '400' }); export const metadata: Metadata = { - title: "ParentGrin Falcon", - description: "Laser focused on your kids", - manifest: "/site.webmanifest", + title: 'ParentGrine Falcon', + description: 'Laser focused on your kids', + manifest: '/site.webmanifest', icons: { - icon: "/favicon.ico", + icon: '/favicon.ico', }, }; @@ -21,9 +27,28 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - -
{children}
+ + + + + + + {children} + + + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 23231f9..00d8fff 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,103 @@ -import { Button } from "@/components/ui/button"; -import Link from "next/link"; +import React from 'react' +import { signIn } from 'next-auth/react' +import { Button, buttonVariants } from '@/components/ui/button' +import { Icons } from '@/components/icons' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import Link from 'next/link' +import { cn } from '@/lib/utils' export default function Home() { return ( -
-

I am home

- -
- ); + <> +
+
+

Track Your Children with Ease

+

+ Parentgrine Falcon 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 Parentgrine Falcon now and stay connected with your loved + ones. +

+ + + Download Now + +
+
+
+

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

+
+ + ) } diff --git a/src/components/children/add-child-component.tsx b/src/components/children/add-child-component.tsx new file mode 100644 index 0000000..e4024df --- /dev/null +++ b/src/components/children/add-child-component.tsx @@ -0,0 +1,58 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Icons } from '../icons'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +const AddChildComponent = () => { + return ( + + + + + + + Add Child + + { + "Enter your child's details below and press save, then use the displayed PIN to register their device." + } + + +
+
+ + +
+
+ + + +
+
+ ); +}; + +export default AddChildComponent; diff --git a/src/components/children/child-select-list.tsx b/src/components/children/child-select-list.tsx new file mode 100644 index 0000000..eac8754 --- /dev/null +++ b/src/components/children/child-select-list.tsx @@ -0,0 +1,50 @@ +'use client'; +import React from 'react'; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import axios from 'axios'; +import { useQuery } from '@tanstack/react-query'; + +const ChildSelectList = () => { + const { data, isLoading, isError } = useQuery({ + queryKey: ['user-children'], + queryFn: async () => { + const { data } = await axios.get( + `${process.env.NEXT_PUBLIC_BASE_URL}/api/child`, + { + withCredentials: true, + } + ); + return data as ChildModel[]; + }, + }); + return ( + + ); +}; + +export default ChildSelectList; diff --git a/src/components/children/children-filter.tsx b/src/components/children/children-filter.tsx new file mode 100644 index 0000000..bce7a62 --- /dev/null +++ b/src/components/children/children-filter.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ChildSelectList from './child-select-list'; + +import AddChildComponent from './add-child-component'; + +const ChildrenFilter = async () => { + return ( +
+ Child + + +
+ ); +}; + +export default ChildrenFilter; diff --git a/src/components/forms/add-child-form.tsx b/src/components/forms/add-child-form.tsx new file mode 100644 index 0000000..c7dedb1 --- /dev/null +++ b/src/components/forms/add-child-form.tsx @@ -0,0 +1,132 @@ +'use client'; + +import * as React from 'react'; +import { useSearchParams } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { signIn } from 'next-auth/react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; + +import { cn } from '@/lib/utils'; +import { newChildSchema } from '@/lib/validations/child'; +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'; + +interface UserAuthFormProps extends React.HTMLAttributes {} + +type FormData = z.infer; + +export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(newChildSchema), + }); + const [isLoading, setIsLoading] = React.useState(false); + const [isGoogleLoading, setIsGoogleLoading] = React.useState(false); + const searchParams = useSearchParams(); + + async function onSubmit(data: FormData) { + setIsLoading(true); + + const signInResult = await fetch( + `${process.env.NEXT_PUBLIC_BASE_URL}/api/child`, + { + method: 'POST', + body: JSON.stringify({ + name: data.name, + }), + } + ); + + setIsLoading(false); + + if (!signInResult?.ok) { + return toast({ + title: 'Something went wrong.', + description: 'Your sign in request failed. Please try again.', + variant: 'destructive', + }); + } + + return toast({ + title: 'Check your email', + description: 'We sent you a login link. Be sure to check your spam too.', + }); + } + + return ( +
+
+
+
+ + + {errors?.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ ); +} diff --git a/src/components/forms/user-auth-form.tsx b/src/components/forms/user-auth-form.tsx new file mode 100644 index 0000000..749f575 --- /dev/null +++ b/src/components/forms/user-auth-form.tsx @@ -0,0 +1,128 @@ +'use client' + +import * as React from 'react' +import { redirect, useSearchParams } from 'next/navigation' +import { zodResolver } from '@hookform/resolvers/zod' +import { signIn } from 'next-auth/react' +import { useForm } from 'react-hook-form' +import * 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' + +interface UserAuthFormProps extends React.HTMLAttributes {} + +type FormData = z.infer + +export function UserAuthForm({ className, ...props }: UserAuthFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(userAuthSchema), + }) + const [isLoading, setIsLoading] = React.useState(false) + const [isGoogleLoading, setIsGoogleLoading] = React.useState(false) + const searchParams = useSearchParams() + + async function onSubmit(data: FormData) { + setIsLoading(true) + + const signInResult = await signIn('email', { + email: data.email.toLowerCase(), + redirect: false, + callbackUrl: searchParams?.get('from') || '/dashboard', + }) + + setIsLoading(false) + + if (!signInResult?.ok) { + return toast({ + title: 'Something went wrong.', + description: 'Your sign in request failed. Please try again.', + variant: 'destructive', + }) + } + + return toast({ + title: 'Check your email', + description: 'We sent you a login link. Be sure to check your spam too.', + }) + } + + return ( +
+
+
+
+ + + {errors?.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ ) +} diff --git a/src/components/header/auth-header.tsx b/src/components/header/auth-header.tsx new file mode 100644 index 0000000..e9514ae --- /dev/null +++ b/src/components/header/auth-header.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Button, buttonVariants } from '@/components/ui/button'; +import { signIn, signOut, useSession } from 'next-auth/react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { UserAvatar } from '@/components/user-avatar'; +import Link from 'next/link'; +import { cn } from '@/lib/utils'; +import { Icons } from '../icons'; + +const AuthHeader = () => { + const { data: session, status } = useSession(); + return !session ? ( + + + Login + + ) : ( + + + + + +
+
+ {session?.user?.name && ( +

{session?.user?.name}

+ )} + {session?.user?.email && ( +

+ {session?.user?.email} +

+ )} +
+
+ + + Dashboard + + + Billing + + + Settings + + + { + event.preventDefault(); + signOut({ + callbackUrl: `${window.location.origin}`, + }); + }} + > + Sign out + +
+
+ ); +}; + +export default AuthHeader; diff --git a/src/components/header/site-header.tsx b/src/components/header/site-header.tsx new file mode 100644 index 0000000..acd9ac8 --- /dev/null +++ b/src/components/header/site-header.tsx @@ -0,0 +1,58 @@ +'use client' +import Link from 'next/link' + +import { siteConfig } from '@/config/site' +import { buttonVariants } from '@/components/ui/button' +import { Icons } from '@/components/icons' +import { MainNav } from '@/components/main-nav' +import { ThemeToggle } from '@/components/header/theme-toggle' +import { useSession } from 'next-auth/react' +import AuthHeader from '@/components/header/auth-header' + +export function SiteHeader() { + const { data: session, status } = useSession() + return ( +
+
+ +
+ +
+
+
+ ) +} diff --git a/src/components/header/theme-toggle.tsx b/src/components/header/theme-toggle.tsx new file mode 100644 index 0000000..c97135e --- /dev/null +++ b/src/components/header/theme-toggle.tsx @@ -0,0 +1,23 @@ +'use client' + +import * as React from 'react' +import { Moon, Sun } from 'lucide-react' +import { useTheme } from 'next-themes' + +import { Button } from '@/components/ui/button' + +export function ThemeToggle() { + const { setTheme, theme } = useTheme() + + return ( + + ) +} diff --git a/src/components/icons.tsx b/src/components/icons.tsx new file mode 100644 index 0000000..3505181 --- /dev/null +++ b/src/components/icons.tsx @@ -0,0 +1,68 @@ +import { + ChevronLeft, + ChevronRight, + LucideIcon, + LucideProps, + TabletSmartphone, + Loader2, + Moon, + PlusCircle, + SunMedium, + Twitter, + User, + Rocket, + PlusCircleIcon, + PlusIcon, + LogIn, +} from 'lucide-react'; + +export type Icon = LucideIcon; + +export const Icons = { + add: PlusIcon, + chevronLeft: ChevronLeft, + chevronRight: ChevronRight, + sun: SunMedium, + login: LogIn, + mobile: TabletSmartphone, + moon: Moon, + rocket: Rocket, + spinner: Loader2, + twitter: Twitter, + user: User, + logo: (props: LucideProps) => ( + + + + ), + gitHub: (props: LucideProps) => ( + + + + ), + google: (props: LucideProps) => ( + + + + ), +}; diff --git a/src/components/main-nav.tsx b/src/components/main-nav.tsx new file mode 100644 index 0000000..c7b8c1e --- /dev/null +++ b/src/components/main-nav.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import Link from 'next/link' + +import { NavItem } from '@/types/nav' +import { siteConfig } from '@/config/site' +import { cn } from '@/lib/utils' +import { Icons } from '@/components/icons' + +interface MainNavProps { + items?: NavItem[] +} + +export function MainNav({ items }: MainNavProps) { + return ( +
+ + + {siteConfig.name} + + {items?.length ? ( + + ) : null} +
+ ) +} diff --git a/src/components/maps/main-map.tsx b/src/components/maps/main-map.tsx index 1a52c89..a41d3c4 100644 --- a/src/components/maps/main-map.tsx +++ b/src/components/maps/main-map.tsx @@ -1,7 +1,7 @@ -"use client"; -import "leaflet/dist/leaflet.css"; -import React from "react"; -import { MapContainer, TileLayer } from "react-leaflet"; +'use client' +import 'leaflet/dist/leaflet.css' +import React from 'react' +import { MapContainer, TileLayer } from 'react-leaflet' const MainMap = () => { return ( @@ -18,7 +18,7 @@ const MainMap = () => { > - ); -}; + ) +} -export default MainMap; +export default MainMap diff --git a/src/components/providers/tanstack-provider.tsx b/src/components/providers/tanstack-provider.tsx new file mode 100644 index 0000000..d4ca177 --- /dev/null +++ b/src/components/providers/tanstack-provider.tsx @@ -0,0 +1,15 @@ +'use client'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import React from 'react'; +const TanstackProvider = ({ children }: { children: React.ReactNode }) => { + const [queryClient] = React.useState(() => new QueryClient()); + return ( + + {children} + + + ); +}; + +export default TanstackProvider; diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..d4b4bbf --- /dev/null +++ b/src/components/theme-provider.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/user-avatar.tsx b/src/components/user-avatar.tsx new file mode 100644 index 0000000..9283cc0 --- /dev/null +++ b/src/components/user-avatar.tsx @@ -0,0 +1,24 @@ +import { AvatarProps } from '@radix-ui/react-avatar' + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Icons } from '@/components/icons' +import { User } from 'next-auth' + +interface UserAvatarProps extends AvatarProps { + user: Pick +} + +export function UserAvatar({ user, ...props }: UserAvatarProps) { + return ( + + {user.image ? ( + + ) : ( + + {user.name} + + + )} + + ) +} diff --git a/src/config/site.ts b/src/config/site.ts new file mode 100644 index 0000000..b3b38cb --- /dev/null +++ b/src/config/site.ts @@ -0,0 +1,20 @@ +export type SiteConfig = typeof siteConfig + +export const siteConfig = { + name: 'Parentgrine Falcon', + description: + 'Free & open source children tracking', + mainNav: [ + { + title: 'Home', + href: '/', + }, { + title: 'Children', + href: '/children', + }, + ], + links: { + twitter: 'https://twitter.com/podnoms', + github: 'https://github.com/parentgrine', + }, +} diff --git a/src/db/schema/auth.ts b/src/db/schema/auth.ts new file mode 100644 index 0000000..769382a --- /dev/null +++ b/src/db/schema/auth.ts @@ -0,0 +1,59 @@ +import { + timestamp, + text, + primaryKey, + integer, + pgTable, +} 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/db/schema/children.ts b/src/db/schema/children.ts new file mode 100644 index 0000000..b127722 --- /dev/null +++ b/src/db/schema/children.ts @@ -0,0 +1,34 @@ +import { + integer, + numeric, + pgEnum, + pgTable, + serial, + uniqueIndex, + uuid, + varchar, +} from 'drizzle-orm/pg-core'; +import { relations, sql } from 'drizzle-orm'; + +export const children = pgTable('children', { + id: uuid('id') + .default(sql`gen_random_uuid()`) + .primaryKey(), + name: varchar('name', { length: 256 }), + phone: varchar('phone', { length: 256 }), +}); +export const locations = pgTable('locations', { + id: uuid('uuid1').defaultRandom().primaryKey(), + longitude: numeric('longitude'), + latitude: numeric('latitude'), + userId: uuid('user_id'), +}); +export const childrenLocations = relations(children, ({ many }) => ({ + locations: many(locations), +})); +export const locationsRelations = relations(locations, ({ one }) => ({ + author: one(children, { + fields: [locations.userId], + references: [children.id], + }), +})); diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts new file mode 100644 index 0000000..48628b1 --- /dev/null +++ b/src/db/schema/index.ts @@ -0,0 +1,7 @@ +import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +// for query purposes +const queryClient = postgres(process.env.DATABASE_URL as string); +const db: PostgresJsDatabase = drizzle(queryClient); +export default db; diff --git a/src/lib/models/child.ts b/src/lib/models/child.ts new file mode 100644 index 0000000..7add430 --- /dev/null +++ b/src/lib/models/child.ts @@ -0,0 +1,4 @@ +interface ChildModel { + name: string + recentLocations: Location[] +} diff --git a/src/lib/models/location.ts b/src/lib/models/location.ts new file mode 100644 index 0000000..02c8e49 --- /dev/null +++ b/src/lib/models/location.ts @@ -0,0 +1,4 @@ +interface Location { + lat: number; + lon: number; +} diff --git a/src/lib/services/auth/config.ts b/src/lib/services/auth/config.ts new file mode 100644 index 0000000..b56796a --- /dev/null +++ b/src/lib/services/auth/config.ts @@ -0,0 +1,16 @@ +import { type AuthOptions } from "next-auth"; +import GoogleProvider from "next-auth/providers/google"; +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import db from "@/db/schema"; + +const authOptions: AuthOptions = { + adapter: DrizzleAdapter(db), + providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + }), + ], +}; + +export default authOptions; diff --git a/src/lib/services/auth/provider.tsx b/src/lib/services/auth/provider.tsx new file mode 100644 index 0000000..dffd619 --- /dev/null +++ b/src/lib/services/auth/provider.tsx @@ -0,0 +1,12 @@ +'use client' + +import { SessionProvider } from 'next-auth/react' +import { ReactNode } from 'react' + +export default function NextAuthProvider({ + children, +}: { + children: ReactNode; +}) { + return {children} +} diff --git a/src/lib/validations/auth.ts b/src/lib/validations/auth.ts new file mode 100644 index 0000000..218890b --- /dev/null +++ b/src/lib/validations/auth.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..2354154 --- /dev/null +++ b/src/lib/validations/child.ts @@ -0,0 +1,5 @@ +import * as z from 'zod'; + +export const newChildSchema = z.object({ + name: z.string().max(50), +}); diff --git a/src/types/nav.ts b/src/types/nav.ts new file mode 100644 index 0000000..0961ce8 --- /dev/null +++ b/src/types/nav.ts @@ -0,0 +1,6 @@ +export interface NavItem { + title: string + href?: string + disabled?: boolean + external?: boolean +} diff --git a/tsconfig.json b/tsconfig.json index e59724b..5d5a724 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -22,6 +22,12 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "drizzle.config.js" + ], "exclude": ["node_modules"] }