mirror of
https://github.com/fergalmoran/kidarr-server.git
synced 2025-12-22 09:17:51 +00:00
First thingy doney
This commit is contained in:
14
.editorconfig
Normal file
14
.editorconfig
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
65
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
12
.idea/dataSources.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
12
.idea/parentgrine-server.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": true
|
||||
}
|
||||
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@@ -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",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
|
||||
14
drizzle.config.ts
Normal file
14
drizzle.config.ts
Normal 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;
|
||||
60
drizzle/0000_thankful_kronos.sql
Normal file
60
drizzle/0000_thankful_kronos.sql
Normal 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 $$;
|
||||
295
drizzle/meta/0000_snapshot.json
Normal file
295
drizzle/meta/0000_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1697494100554,
|
||||
"tag": "0000_thankful_kronos",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
env: {},
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
||||
34
package.json
34
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
15
scripts/reset.sh
Executable 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
|
||||
47
src/app/(auth)/signin/page.tsx
Normal file
47
src/app/(auth)/signin/page.tsx
Normal 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't have an account? Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SigninPage;
|
||||
19
src/app/(dashboard)/dashboard/page.tsx
Normal file
19
src/app/(dashboard)/dashboard/page.tsx
Normal 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
|
||||
15
src/app/(dashboard)/layout.tsx
Normal file
15
src/app/(dashboard)/layout.tsx
Normal 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
|
||||
5
src/app/api/auth/[...nextauth]/route.ts
Normal file
5
src/app/api/auth/[...nextauth]/route.ts
Normal 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 }
|
||||
18
src/app/api/child/route.ts
Normal file
18
src/app/api/child/route.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
108
src/app/page.tsx
108
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 (
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
58
src/components/children/add-child-component.tsx
Normal file
58
src/components/children/add-child-component.tsx
Normal 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;
|
||||
50
src/components/children/child-select-list.tsx
Normal file
50
src/components/children/child-select-list.tsx
Normal 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;
|
||||
16
src/components/children/children-filter.tsx
Normal file
16
src/components/children/children-filter.tsx
Normal 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;
|
||||
132
src/components/forms/add-child-form.tsx
Normal file
132
src/components/forms/add-child-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
128
src/components/forms/user-auth-form.tsx
Normal file
128
src/components/forms/user-auth-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
77
src/components/header/auth-header.tsx
Normal file
77
src/components/header/auth-header.tsx
Normal 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;
|
||||
58
src/components/header/site-header.tsx
Normal file
58
src/components/header/site-header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
23
src/components/header/theme-toggle.tsx
Normal file
23
src/components/header/theme-toggle.tsx
Normal 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
68
src/components/icons.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
41
src/components/main-nav.tsx
Normal file
41
src/components/main-nav.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
15
src/components/providers/tanstack-provider.tsx
Normal file
15
src/components/providers/tanstack-provider.tsx
Normal 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;
|
||||
9
src/components/theme-provider.tsx
Normal file
9
src/components/theme-provider.tsx
Normal 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>
|
||||
}
|
||||
24
src/components/user-avatar.tsx
Normal file
24
src/components/user-avatar.tsx
Normal 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
20
src/config/site.ts
Normal 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
59
src/db/schema/auth.ts
Normal 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
34
src/db/schema/children.ts
Normal 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
7
src/db/schema/index.ts
Normal 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
4
src/lib/models/child.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
interface ChildModel {
|
||||
name: string
|
||||
recentLocations: Location[]
|
||||
}
|
||||
4
src/lib/models/location.ts
Normal file
4
src/lib/models/location.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
interface Location {
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
16
src/lib/services/auth/config.ts
Normal file
16
src/lib/services/auth/config.ts
Normal 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;
|
||||
12
src/lib/services/auth/provider.tsx
Normal file
12
src/lib/services/auth/provider.tsx
Normal 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>
|
||||
}
|
||||
5
src/lib/validations/auth.ts
Normal file
5
src/lib/validations/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const userAuthSchema = z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
5
src/lib/validations/child.ts
Normal file
5
src/lib/validations/child.ts
Normal 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
6
src/types/nav.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface NavItem {
|
||||
title: string
|
||||
href?: string
|
||||
disabled?: boolean
|
||||
external?: boolean
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user