First thingy doney

This commit is contained in:
Fergal Moran
2023-10-16 23:18:27 +01:00
parent 4e1ba1c0ba
commit 24e26177a5
55 changed files with 1755 additions and 107 deletions

14
.editorconfig Normal file
View File

@@ -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

8
.idea/.gitignore generated vendored Normal file
View File

@@ -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

65
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,65 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_KEEP_WHITESPACES_INSIDE" value="" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="parentgrine@localhost" uuid="2154c69f-8b13-41df-9abf-71df677c19e4">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/parentgrine</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/parentgrine-server.iml" filepath="$PROJECT_DIR$/.idea/parentgrine-server.iml" />
</modules>
</component>
</project>

12
.idea/parentgrine-server.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"singleAttributePerLine": true
}

14
.vscode/settings.json vendored
View File

@@ -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",

BIN
bun.lockb

Binary file not shown.

View File

@@ -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"
}
}
}

14
drizzle.config.ts Normal file
View File

@@ -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;

View File

@@ -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 $$;

View File

@@ -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": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1697494100554,
"tag": "0000_thankful_kronos",
"breakpoints": true
}
]
}

View File

@@ -1,4 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
env: {},
};
module.exports = nextConfig
module.exports = nextConfig;

View File

@@ -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"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "Parent Grin",
"short_name": "parentgrin",
"short_name": "parentgrine",
"icons": [
{
"src": "/android-chrome-192x192.png",

15
scripts/reset.sh Executable file
View File

@@ -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

View File

@@ -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 (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: 'ghost' }),
'absolute left-4 top-4 md:left-8 md:top-8'
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Enter your email to sign in to your account
</p>
</div>
<UserAuthForm />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/register"
className="hover:text-brand underline underline-offset-4"
>
Don&apos;t have an account? Sign Up
</Link>
</p>
</div>
</div>
);
};
export default SigninPage;

View File

@@ -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 (
<div>
<div className="z-10">
<ChildrenFilter />
</div>
<div>This is the dashboard</div>
<div className="z-0">
<MainMap />
</div>
</div>
)
}
export default DashboardPage

View File

@@ -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 (
<div className="flex min-h-screen flex-col space-y-6">
<SiteHeader />
<div className="mx-6">{children}</div>
</div>
)
}
export default DashboardLayout

View File

@@ -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 }

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 (
<html lang="en">
<body className={inter.className}>
<div className="mx-auto p-12">{children}</div>
<html
lang="en"
suppressHydrationWarning
>
<head />
<body
className={cn(
'min-h-screen bg-background font-sans antialiased',
font.className
)}
>
<NextAuthProvider>
<TanstackProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
</TanstackProvider>
</NextAuthProvider>
</body>
</html>
);

View File

@@ -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 (
<main className="flex min-h-screen flex-col items-center justify-start space-y-6 p-24">
<h1>I am home</h1>
<Button asChild variant={"default"}>
<Link href="/map">Map me</Link>
</Button>
</main>
);
<>
<section className="py-16">
<div className="container mx-auto text-center">
<h2 className="text-4xl font-bold">Track Your Children with Ease</h2>
<p className="text-lg mt-4 text-muted-foreground">
Parentgrine Falcon helps you keep an eye on your loved ones and
ensure their safety.
</p>
<Link
className={cn(
buttonVariants({ variant: 'default', size: 'lg' }),
'mt-8'
)}
href="/signin"
>
<Icons.rocket className="mr-2 h-4 w-4" /> {"Let's go"}
</Link>
</div>
</section>
<section className="py-16">
<div className="container mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
<Card>
<CardHeader>
<CardTitle>Real-Time Location Tracking</CardTitle>
</CardHeader>
<CardContent>
<p>
Instantly know where your children are at all times with
accurate GPS tracking.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Geofencing Alerts</CardTitle>
</CardHeader>
<CardContent>
<p>
Receive notifications when your child enters or leaves
designated safe zones.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Activity Monitoring</CardTitle>
</CardHeader>
<CardContent>
<p>
{
"View your child's activity history, including visited places and routes taken."
}
</p>
</CardContent>
</Card>
</div>
</section>
<section className="bg-background py-16">
<div className="container mx-auto text-center">
<h3 className="text-3xl font-semibold">
Keep Your Children Safe Today!
</h3>
<p className="text-lg mt-4 text-muted-foreground">
Download Parentgrine Falcon now and stay connected with your loved
ones.
</p>
<Link
className={cn(
buttonVariants({ variant: 'outline', size: 'lg' }),
'mt-8'
)}
href="/download"
>
<Icons.mobile className="mr-2 h-4 w-4" /> Download Now
</Link>
</div>
</section>
<footer className="bg-secondary-foreground text-secondary py-8 text-center">
<p>
An open source experiment from PodNoms - source code available{' '}
<Link
target="_blank"
href="https://github.com/parentgrine"
>
here
</Link>
</p>
</footer>
</>
)
}

View File

@@ -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 (
<Dialog>
<DialogTrigger asChild>
<Button
variant={'default'}
size={'sm'}
>
<Icons.add className="mr-2 h-4 w-4" /> Add child
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add Child</DialogTitle>
<DialogDescription>
{
"Enter your child's details below and press save, then use the displayed PIN to register their device."
}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="name"
className="text-right"
>
Name
</Label>
<Input
id="name"
placeholder={"Your child's identifier"}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default AddChildComponent;

View File

@@ -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 (
<Select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Choose child" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="____all____">(All Children)</SelectItem>
{data?.map((r) => (
<SelectItem
key={r.name}
value={r.name.toLowerCase()}
>
{r.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
);
};
export default ChildSelectList;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import ChildSelectList from './child-select-list';
import AddChildComponent from './add-child-component';
const ChildrenFilter = async () => {
return (
<div className="flex flex-row space-x-2 justify-center items-center">
<span>Child</span>
<ChildSelectList />
<AddChildComponent />
</div>
);
};
export default ChildrenFilter;

View File

@@ -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<HTMLDivElement> {}
type FormData = z.infer<typeof newChildSchema>;
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(newChildSchema),
});
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [isGoogleLoading, setIsGoogleLoading] = React.useState<boolean>(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 (
<div
className={cn('grid gap-6', className)}
{...props}
>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label
className="sr-only"
htmlFor="email"
>
Email
</Label>
<Input
id="email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading || isGoogleLoading}
{...register('email')}
/>
{errors?.email && (
<p className="px-1 text-xs text-red-600">
{errors.email.message}
</p>
)}
</div>
<button
className={cn(buttonVariants())}
disabled={isLoading}
>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign In with Email
</button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<button
type="button"
className={cn(buttonVariants({ variant: 'outline' }))}
onClick={() => {
setIsGoogleLoading(true);
signIn('google', { callbackUrl: '/dashboard' });
}}
disabled={isLoading || isGoogleLoading}
>
{isGoogleLoading ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<Icons.google className="mr-2 h-4 w-4" />
)}{' '}
Google
</button>
</div>
);
}

View File

@@ -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<HTMLDivElement> {}
type FormData = z.infer<typeof userAuthSchema>
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(userAuthSchema),
})
const [isLoading, setIsLoading] = React.useState<boolean>(false)
const [isGoogleLoading, setIsGoogleLoading] = React.useState<boolean>(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 (
<div
className={cn('grid gap-6', className)}
{...props}
>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-2">
<div className="grid gap-1">
<Label
className="sr-only"
htmlFor="email"
>
Email
</Label>
<Input
id="email"
placeholder="name@example.com"
type="email"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading || isGoogleLoading}
{...register('email')}
/>
{errors?.email && (
<p className="px-1 text-xs text-red-600">
{errors.email.message}
</p>
)}
</div>
<button
className={cn(buttonVariants())}
disabled={isLoading}
>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign In with Email
</button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<button
type="button"
className={cn(buttonVariants({ variant: 'outline' }))}
onClick={() => {
setIsGoogleLoading(true)
signIn('google', { callbackUrl: '/dashboard' })
}}
disabled={isLoading || isGoogleLoading}
>
{isGoogleLoading ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<Icons.google className="mr-2 h-4 w-4" />
)}{' '}
Google
</button>
</div>
)
}

View File

@@ -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 ? (
<Link
className={cn(buttonVariants({ variant: 'default', size: 'sm' }))}
href="/signin"
>
<Icons.login className="mr-2 h-4 w-4" />
Login
</Link>
) : (
<DropdownMenu>
<DropdownMenuTrigger>
<UserAvatar
user={{
name: session?.user?.name || null,
image: session?.user?.image || null,
}}
className="h-6 w-6"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
{session?.user?.name && (
<p className="font-medium">{session?.user?.name}</p>
)}
{session?.user?.email && (
<p className="w-[200px] truncate text-sm text-muted-foreground">
{session?.user?.email}
</p>
)}
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">Dashboard</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/billing">Billing</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/settings">Settings</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onSelect={(event) => {
event.preventDefault();
signOut({
callbackUrl: `${window.location.origin}`,
});
}}
>
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default AuthHeader;

View File

@@ -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 (
<header className='bg-background sticky top-0 z-40 w-full border-b'>
<div
className='container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0'>
<MainNav items={siteConfig.mainNav} />
<div className='flex flex-1 items-center justify-end space-x-4'>
<nav className='flex items-center space-x-1'>
<Link
href={siteConfig.links.github}
target='_blank'
rel='noreferrer'
>
<div
className={buttonVariants({
size: 'icon',
variant: 'ghost',
})}
>
<Icons.gitHub className='h-5 w-5' />
<span className='sr-only'>GitHub</span>
</div>
</Link>
<Link
href={siteConfig.links.twitter}
target='_blank'
rel='noreferrer'
>
<div
className={buttonVariants({
size: 'icon',
variant: 'ghost',
})}
>
<Icons.twitter className='h-5 w-5 fill-current' />
<span className='sr-only'>Twitter</span>
</div>
</Link>
<ThemeToggle />
<AuthHeader />
</nav>
</div>
</div>
</header>
)
}

View File

@@ -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 (
<Button
variant='ghost'
size='icon'
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
>
<Sun className='h-[1.5rem] w-[1.3rem] dark:hidden' />
<Moon className='hidden h-5 w-5 dark:block' />
<span className='sr-only'>Toggle theme</span>
</Button>
)
}

68
src/components/icons.tsx Normal file
View File

@@ -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) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M11.572 0c-.176 0-.31.001-.358.007a19.76 19.76 0 0 1-.364.033C7.443.346 4.25 2.185 2.228 5.012a11.875 11.875 0 0 0-2.119 5.243c-.096.659-.108.854-.108 1.747s.012 1.089.108 1.748c.652 4.506 3.86 8.292 8.209 9.695.779.25 1.6.422 2.534.525.363.04 1.935.04 2.299 0 1.611-.178 2.977-.577 4.323-1.264.207-.106.247-.134.219-.158-.02-.013-.9-1.193-1.955-2.62l-1.919-2.592-2.404-3.558a338.739 338.739 0 0 0-2.422-3.556c-.009-.002-.018 1.579-.023 3.51-.007 3.38-.01 3.515-.052 3.595a.426.426 0 0 1-.206.214c-.075.037-.14.044-.495.044H7.81l-.108-.068a.438.438 0 0 1-.157-.171l-.05-.106.006-4.703.007-4.705.072-.092a.645.645 0 0 1 .174-.143c.096-.047.134-.051.54-.051.478 0 .558.018.682.154.035.038 1.337 1.999 2.895 4.361a10760.433 10760.433 0 0 0 4.735 7.17l1.9 2.879.096-.063a12.317 12.317 0 0 0 2.466-2.163 11.944 11.944 0 0 0 2.824-6.134c.096-.66.108-.854.108-1.748 0-.893-.012-1.088-.108-1.747-.652-4.506-3.859-8.292-8.208-9.695a12.597 12.597 0 0 0-2.499-.523A33.119 33.119 0 0 0 11.573 0zm4.069 7.217c.347 0 .408.005.486.047a.473.473 0 0 1 .237.277c.018.06.023 1.365.018 4.304l-.006 4.218-.744-1.14-.746-1.14v-3.066c0-1.982.01-3.097.023-3.15a.478.478 0 0 1 .233-.296c.096-.05.13-.054.5-.054z"
/>
</svg>
),
gitHub: (props: LucideProps) => (
<svg
viewBox="0 0 438.549 438.549"
{...props}
>
<path
fill="currentColor"
d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"
></path>
</svg>
),
google: (props: LucideProps) => (
<svg
role="img"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
/>
</svg>
),
};

View File

@@ -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 (
<div className='flex gap-6 md:gap-10'>
<Link href='/' className='flex items-center space-x-2'>
<Icons.logo className='h-6 w-6' />
<span className='inline-block font-bold'>{siteConfig.name}</span>
</Link>
{items?.length ? (
<nav className='flex gap-6'>
{items?.map(
(item, index) =>
item.href && (
<Link
key={index}
href={item.href}
className={cn(
'flex items-center text-sm font-medium text-muted-foreground',
item.disabled && 'cursor-not-allowed opacity-80',
)}
>
{item.title}
</Link>
),
)}
</nav>
) : null}
</div>
)
}

View File

@@ -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 = () => {
></TileLayer>
</MapContainer>
</div>
);
};
)
}
export default MainMap;
export default MainMap

View File

@@ -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 (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default TanstackProvider;

View File

@@ -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 <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -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<User, 'image' | 'name'>
}
export function UserAvatar({ user, ...props }: UserAvatarProps) {
return (
<Avatar {...props}>
{user.image ? (
<AvatarImage alt='Picture' src={user.image} />
) : (
<AvatarFallback>
<span className='sr-only'>{user.name}</span>
<Icons.user className='h-4 w-4' />
</AvatarFallback>
)}
</Avatar>
)
}

20
src/config/site.ts Normal file
View File

@@ -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',
},
}

59
src/db/schema/auth.ts Normal file
View File

@@ -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<AdapterAccount['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),
}),
)

34
src/db/schema/children.ts Normal file
View File

@@ -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],
}),
}));

7
src/db/schema/index.ts Normal file
View File

@@ -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;

4
src/lib/models/child.ts Normal file
View File

@@ -0,0 +1,4 @@
interface ChildModel {
name: string
recentLocations: Location[]
}

View File

@@ -0,0 +1,4 @@
interface Location {
lat: number;
lon: number;
}

View File

@@ -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;

View File

@@ -0,0 +1,12 @@
'use client'
import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'
export default function NextAuthProvider({
children,
}: {
children: ReactNode;
}) {
return <SessionProvider>{children}</SessionProvider>
}

View File

@@ -0,0 +1,5 @@
import * as z from 'zod'
export const userAuthSchema = z.object({
email: z.string().email(),
})

View File

@@ -0,0 +1,5 @@
import * as z from 'zod';
export const newChildSchema = z.object({
name: z.string().max(50),
});

6
src/types/nav.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface NavItem {
title: string
href?: string
disabled?: boolean
external?: boolean
}

View File

@@ -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"]
}