diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index c061075..de82a8c 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -17,6 +17,7 @@ const config = {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-non-null-asserted-optional-chain": ["off"],
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 9fbc5b9..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
deleted file mode 100644
index 800272b..0000000
--- a/.idea/dataSources.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- postgresql
- true
- org.postgresql.Driver
- jdbc:postgresql://localhost:5432/parentgrine
- $ProjectFileDir$
-
-
-
\ No newline at end of file
diff --git a/.idea/parentgrine-server.iml b/.idea/kidarr-server.iml
similarity index 100%
rename from .idea/parentgrine-server.iml
rename to .idea/kidarr-server.iml
diff --git a/.idea/modules.xml b/.idea/modules.xml
index a862b72..9fe0d47 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 0000000..0c83ac4
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1dd..94a25f7 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index e3a678d..0000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Next.js: debug server-side",
- "type": "node-terminal",
- "request": "launch",
- "command": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --config ./ssl-proxy.json"
- }
- ]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 908255f..4001a19 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,26 @@
{
- "cmake.configureOnOpen": false,
- "workbench.editor.labelFormat": "short"
+ "files.exclude": {
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true,
+ ".vscode": true,
+ ".next": true,
+ "node_modules": true
+ },
+ "workbench.colorTheme": "poimandres-noitalics",
+ "sqltools.connections": [
+ {
+ "previewLimit": 50,
+ "server": "localhost",
+ "port": 5432,
+ "driver": "PostgreSQL",
+ "name": "kidarr (local)",
+ "database": "kidarr",
+ "username": "postgres",
+ "password": "hackme"
+ }
+ ]
}
diff --git a/README.md b/README.md
index aa85ddd..fba19ed 100644
--- a/README.md
+++ b/README.md
@@ -1,64 +1,28 @@
-# Kidarr Serverπ
+# Create T3 App
-Welcome to Kidarr, the ultimate child location tracking app that prioritises safety and peace of mind for parents!
-π
+This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
-[](LICENSE)
-[](https://github.com/kid-arr/kidarr-server/issues)
-[](https://github.com/kid-arr/kidarr-server/stargazers)
-[](https://github.com/kid-arr/kidarr-server/network)
+## What's next? How do I make an app with this?
-## Overview
+We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary.
-Kidarr is a secure and user-friendly mobile application designed to help parents keep track of their children's
-whereabouts in real-time. With advanced location tracking features, intuitive UI, and robust security measures,
-Kidarr provides parents with the peace of mind they deserve.
+If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
-## Features
+- [Next.js](https://nextjs.org)
+- [NextAuth.js](https://next-auth.js.org)
+- [Prisma](https://prisma.io)
+- [Tailwind CSS](https://tailwindcss.com)
+- [tRPC](https://trpc.io)
-- **Real-time Location Tracking**: Stay informed about your child's location at all times.
+## Learn More
-- **Geofencing**: Set up safe zones and receive notifications when your child enters or leaves predefined areas.
+To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
-- **SOS Alert**: In case of an emergency, your child can send an SOS alert with their current location.
+- [Documentation](https://create.t3.gg/)
+- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) β Check out these awesome tutorials
-- **History Tracking**: View a detailed history of your child's movement over time.
+You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) β your feedback and contributions are welcome!
-- **Privacy and Security**: We prioritize the privacy and security of your data. All communication is encrypted, and
- location data is accessible only to authorized users.
+## How do I deploy this?
-## Getting Started
-
-### Prerequisites
-
-- iOS or Android device for your child
-- Web, iOS or Android device for the parent
-
-### Installation
-
-1. Clone the repository: `git clone https://github.com/kid-arr/kidarr-server.git`
-2. Follow the installation instructions in the [documentation](docs/INSTALL.md).
-
-## Usage
-
-1. Open the Kidarr app on your child's device.
-2. Log in with your parent account.
-3. Enjoy peace of mind by tracking your child's location.
-
-For more detailed instructions, check out our [User Guide](docs/UserGuide.md).
-
-## Contributing
-
-We welcome contributions from the community! Whether you're a developer, designer, or tester, your input is valuable.
-Check out our [contribution guidelines](CONTRIBUTING.md) for more information.
-
-## Support
-
-If you encounter any issues or have questions, feel free
-to [open an issue](https://github.com/kid-arr/kidarr-server/issues). We're here to help!
-
-## License
-
-Kidarr is licensed under the [MIT License](LICENSE).
-
-Happy tracking! πβ¨
+Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
diff --git a/bun.lockb b/bun.lockb
index 057101d..ca4ce00 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components.json b/components.json
index e3c2bcf..33fb800 100644
--- a/components.json
+++ b/components.json
@@ -5,10 +5,9 @@
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
- "css": "styles/globals.css",
- "baseColor": "zinc",
- "cssVariables": true,
- "prefix": ""
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true
},
"aliases": {
"components": "@/components",
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 811ffc6..cab1b8b 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -1,15 +1,11 @@
-import type { Config } from 'drizzle-kit';
-import { env } from '@/env';
+import type { Config } from "drizzle-kit";
+import { env } from "@/env.mjs";
-if (!env.DATABASE_URL) {
- throw new Error('DATABASE_URL is missing');
-}
export default {
- // schema: './src/db',
- schema: './src/server/db/schema.ts',
- out: './drizzle',
- driver: 'pg',
+ schema: "./src/server/db/schema",
+ out: "./src/server/db/migrations",
+ driver: "pg",
dbCredentials: {
connectionString: env.DATABASE_URL,
- },
-} satisfies Config;
+ }
+} satisfies Config;
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
deleted file mode 100644
index 1952f15..0000000
--- a/drizzle/meta/_journal.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "version": "5",
- "dialect": "pg",
- "entries": [
- {
- "idx": 0,
- "version": "5",
- "when": 1705356382861,
- "tag": "0000_wide_gravity",
- "breakpoints": true
- }
- ]
-}
\ No newline at end of file
diff --git a/kirimase.config.json b/kirimase.config.json
new file mode 100644
index 0000000..886951d
--- /dev/null
+++ b/kirimase.config.json
@@ -0,0 +1,18 @@
+{
+ "hasSrc": true,
+ "packages": [
+ "trpc",
+ "shadcn-ui",
+ "drizzle",
+ "next-auth"
+ ],
+ "preferredPackageManager": "bun",
+ "t3": true,
+ "alias": "@",
+ "rootPath": "src/",
+ "componentLib": "shadcn-ui",
+ "driver": "pg",
+ "provider": "postgresjs",
+ "orm": "drizzle",
+ "auth": "next-auth"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index f0e56d8..7c76479 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,26 @@
{
- "name": "kidarr",
- "version": "0.1.5",
+ "name": "kidarr-server",
+ "version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
- "db:push": "dotenv drizzle-kit push:postgres",
- "db:studio": "dotenv drizzle-kit studio",
+ "_dev": "next dev",
"dev": "NODE_ENV=development next dev -p 3002 & local-ssl-proxy --config ./ssl-proxy.json",
"lint": "next lint",
- "start": "next start"
+ "start": "next start",
+ "db:generate": "drizzle-kit generate:pg",
+ "db:migrate": "tsx src/server/db/migrate.ts",
+ "db:seed": "tsx src/server/db/seed/seed.ts && tsx src/server/db/seed/auth.ts",
+ "db:drop": "drizzle-kit drop",
+ "db:pull": "drizzle-kit introspect:pg",
+ "db:studio": "drizzle-kit studio",
+ "db:check": "drizzle-kit check:pg"
},
"dependencies": {
- "@auth/drizzle-adapter": "^0.3.14",
- "@faker-js/faker": "^8.3.1",
+ "@auth/core": "^0.22.0",
+ "@auth/drizzle-adapter": "^0.3.16",
+ "@faker-js/faker": "^8.4.0",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
@@ -44,26 +51,26 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@t3-oss/env-nextjs": "^0.7.3",
"@tanstack/react-query": "^4.36.1",
- "@trpc/client": "^10.45.0",
- "@trpc/next": "^10.45.0",
- "@trpc/react-query": "^10.45.0",
- "@trpc/server": "^10.45.0",
- "@vercel/analytics": "^1.1.1",
+ "@trpc/client": "^10.43.6",
+ "@trpc/next": "^10.43.6",
+ "@trpc/react-query": "^10.43.6",
+ "@trpc/server": "^10.43.6",
+ "@types/leaflet": "^1.9.8",
+ "@types/react-leaflet": "^3.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
- "date-fns": "^3.2.0",
+ "date-fns": "^3.3.1",
"drizzle-orm": "^0.29.3",
- "embla-carousel-react": "^8.0.0-rc19",
- "generate-api-key": "^1.0.2",
+ "drizzle-zod": "^0.5.1",
+ "embla-carousel-react": "^8.0.0-rc20",
"http-status-codes": "^2.3.0",
"leaflet": "^1.9.4",
"local-ssl-proxy": "^2.0.5",
- "lucide-react": "^0.309.0",
+ "lucide-react": "^0.314.0",
"next": "^14.0.4",
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
- "pg": "^8.11.3",
"postgres": "^3.4.3",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
@@ -79,29 +86,30 @@
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
- "vaul": "^0.8.0",
+ "vaul": "^0.8.9",
"zod": "^3.22.4"
},
"devDependencies": {
- "@types/leaflet": "^1.9.8",
- "@next/eslint-plugin-next": "^14.0.4",
- "@types/eslint": "^8.56.2",
- "@types/node": "^20.11.0",
- "@types/react": "^18.2.47",
- "@types/react-dom": "^18.2.18",
- "@typescript-eslint/eslint-plugin": "^6.18.1",
- "@typescript-eslint/parser": "^6.18.1",
- "autoprefixer": "^10.4.16",
- "dotenv-cli": "^7.3.0",
- "drizzle-kit": "^0.20.12",
- "eslint": "^8.56.0",
- "postcss": "^8.4.33",
- "prettier": "^3.2.2",
- "prettier-plugin-tailwindcss": "^0.5.11",
- "tailwindcss": "^3.4.1",
- "typescript": "^5.3.3"
+ "@types/eslint": "^8.44.7",
+ "@types/node": "^18.17.0",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@typescript-eslint/eslint-plugin": "^6.11.0",
+ "@typescript-eslint/parser": "^6.11.0",
+ "autoprefixer": "^10.4.14",
+ "dotenv": "^16.3.2",
+ "drizzle-kit": "^0.20.13",
+ "eslint": "^8.54.0",
+ "eslint-config-next": "^14.0.4",
+ "pg": "^8.11.3",
+ "postcss": "^8.4.31",
+ "prettier": "^3.1.0",
+ "prettier-plugin-tailwindcss": "^0.5.7",
+ "tailwindcss": "^3.3.5",
+ "tsx": "^4.7.0",
+ "typescript": "^5.1.6"
},
"ct3aMetadata": {
- "initVersion": "7.25.0"
+ "initVersion": "7.25.2"
}
}
diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
deleted file mode 100644
index cbd81c1..0000000
Binary files a/public/android-chrome-192x192.png and /dev/null differ
diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
deleted file mode 100644
index 8d144a0..0000000
Binary files a/public/android-chrome-512x512.png and /dev/null differ
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
deleted file mode 100644
index a033189..0000000
Binary files a/public/apple-touch-icon.png and /dev/null differ
diff --git a/public/favicon copy.ico b/public/favicon copy.ico
deleted file mode 100644
index efb824e..0000000
Binary files a/public/favicon copy.ico and /dev/null differ
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
deleted file mode 100644
index c31bb38..0000000
Binary files a/public/favicon-16x16.png and /dev/null differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
deleted file mode 100644
index dc9110b..0000000
Binary files a/public/favicon-32x32.png and /dev/null differ
diff --git a/public/img/default-avatar.png b/public/img/default-avatar.png
deleted file mode 100644
index eafe9f4..0000000
Binary files a/public/img/default-avatar.png and /dev/null differ
diff --git a/public/site.webmanifest b/public/site.webmanifest
deleted file mode 100644
index 25e63ff..0000000
--- a/public/site.webmanifest
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "name": "Kidarr - radar for your kids",
- "short_name": "kidarr",
- "icons": [
- {
- "src": "/android-chrome-192x192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "/android-chrome-512x512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
- "display": "standalone"
-}
diff --git a/scripts/queries.sql b/scripts/queries.sql
new file mode 100644
index 0000000..78422f2
--- /dev/null
+++ b/scripts/queries.sql
@@ -0,0 +1 @@
+select * From pings
\ No newline at end of file
diff --git a/scripts/reset.sh b/scripts/reset.sh
index 9f4b23a..1050b78 100755
--- a/scripts/reset.sh
+++ b/scripts/reset.sh
@@ -11,9 +11,8 @@ dropdb -f --if-exists kidarr
echo "Creating db"
createdb kidarr
-bunx drizzle-kit generate:pg --config=./drizzle.config.ts
-bunx drizzle-kit push:pg --config=./drizzle.config.ts
+bun db:generate
+bun db:migrate
# bun run src/db/migrate.ts
-bun run ./src/server/db/scripts/seed.ts
-bun run ./src/server/db/scripts/auth.ts
+bun db:seed
diff --git a/src/app/(app)/children/page.tsx b/src/app/(app)/children/page.tsx
new file mode 100644
index 0000000..b2dd468
--- /dev/null
+++ b/src/app/(app)/children/page.tsx
@@ -0,0 +1,19 @@
+import ChildList from "@/components/children/child-list";
+import NewChildModal from "@/components/children/child-modal";
+import { api } from "@/trpc/server";
+import { checkAuth } from "@/lib/auth/utils";
+
+export default async function Children() {
+ await checkAuth();
+ const { children } = await api.children.getChildren.query();
+
+ return (
+
+
+
Here are your children.
+
+
+
+
+ );
+}
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx
new file mode 100644
index 0000000..279597b
--- /dev/null
+++ b/src/app/(app)/dashboard/page.tsx
@@ -0,0 +1,10 @@
+import DashboardPage from "@/components/pages/dashboard-page";
+import { checkAuth } from "@/lib/auth/utils";
+import React from "react";
+
+const Dashboard = async () => {
+ await checkAuth();
+ return ;
+};
+
+export default Dashboard;
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
new file mode 100644
index 0000000..30ec414
--- /dev/null
+++ b/src/app/(app)/layout.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { SiteHeader } from '@/components/header/site-header';
+import { SocketProvider } from '@/lib/services/realtime/socket-provider';
+
+type DashboardLayoutProps = {
+ children?: React.ReactNode
+}
+const AppLayout = async ({ children }: DashboardLayoutProps) => {
+ return (
+
+
+
+ );
+
+};
+export default AppLayout;
diff --git a/src/app/(debug)/debug/page.tsx b/src/app/(debug)/debug/page.tsx
deleted file mode 100644
index f2070aa..0000000
--- a/src/app/(debug)/debug/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { SecureDebugDetails } from "@/components/debug/SecureDebugDetails";
-import React from "react";
-import HeadersPrinter from "@/components/debug/HeadersPrinter";
-
-const DebugPage = async () => {
- return (
- <>
-
- This is what we know
-
-
-
-
-
- >
- );
-};
-export default DebugPage;
diff --git a/src/app/(parent)/children/page.tsx b/src/app/(parent)/children/page.tsx
deleted file mode 100644
index 6803a32..0000000
--- a/src/app/(parent)/children/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import ChildrenList from '@/components/children/children-list';
-import { api } from '@/trpc/server';
-
-const ChildrenPage = async () => {
- const kids = await api.child.mine.query();
- return ;
-};
-
-export default ChildrenPage;
diff --git a/src/app/(parent)/dashboard/page.tsx b/src/app/(parent)/dashboard/page.tsx
deleted file mode 100644
index 09063c1..0000000
--- a/src/app/(parent)/dashboard/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import { api } from '@/trpc/server';
-import ChildrenFilter from '@/components/children/children-filter';
-import dynamic from 'next/dynamic';
-import { MapViewTypeSelector } from '@/components/maps/map-viewtype-selector';
-
-const Dashboard = async () => {
- const kids = await api.child.mine.query();
- const Map = dynamic(() => import('@/components/maps/main-map'), {
- ssr: false,
- });
- return ;
-};
-
-export default Dashboard;
diff --git a/src/app/(parent)/layout.tsx b/src/app/(parent)/layout.tsx
deleted file mode 100644
index 6344c39..0000000
--- a/src/app/(parent)/layout.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { SiteHeader } from '@/components/header/site-header';
-import React from 'react';
-import { SocketProvider } from '@/lib/services/realtime/socket-provider';
-
-type DashboardLayoutProps = {
- children?: React.ReactNode
-}
-const DashboardLayout = async ({ children }: DashboardLayoutProps) => {
- return (
-
-
-
- );
-
-};
-export default DashboardLayout;
diff --git a/src/app/account/AccountCard.tsx b/src/app/account/AccountCard.tsx
new file mode 100644
index 0000000..2e37f10
--- /dev/null
+++ b/src/app/account/AccountCard.tsx
@@ -0,0 +1,45 @@
+import { Card } from "@/components/ui/card";
+
+interface AccountCardProps {
+ params: {
+ header: string;
+ description: string;
+ price?: number;
+ };
+ children: React.ReactNode;
+}
+
+export function AccountCard({ params, children }: AccountCardProps) {
+ const { header, description } = params;
+ return (
+
+
+
{header}
+
{description}
+
+ {children}
+
+ );
+}
+
+export function AccountCardBody({ children }: { children: React.ReactNode }) {
+ return {children}
;
+}
+
+export function AccountCardFooter({
+ description,
+ children,
+}: {
+ children: React.ReactNode;
+ description: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/app/account/UpdateEmailCard.tsx b/src/app/account/UpdateEmailCard.tsx
new file mode 100644
index 0000000..99ec6af
--- /dev/null
+++ b/src/app/account/UpdateEmailCard.tsx
@@ -0,0 +1,56 @@
+import { AccountCard, AccountCardFooter, AccountCardBody } from "./AccountCard";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useToast } from "@/components/ui/use-toast";
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+
+export default function UpdateEmailCard({ email }: { email: string }) {
+ const { toast } = useToast();
+ const [isPending, startTransition] = useTransition();
+ const router = useRouter();
+
+ const handleSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+ const target = event.target as HTMLFormElement;
+ const form = new FormData(target);
+ const { email } = Object.fromEntries(form.entries()) as { email: string };
+ if (email.length < 3) {
+ toast({
+ description: "Email must be longer than 3 characters.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ startTransition(async () => {
+ const res = await fetch("/api/account", {
+ method: "PUT",
+ body: JSON.stringify({ email }),
+ headers: { "Content-Type": "application/json" },
+ });
+ if (res.status === 200)
+ toast({ description: "Successfully updated email!" });
+ router.refresh();
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/account/UpdateNameCard.tsx b/src/app/account/UpdateNameCard.tsx
new file mode 100644
index 0000000..228cf03
--- /dev/null
+++ b/src/app/account/UpdateNameCard.tsx
@@ -0,0 +1,56 @@
+"use client";
+import { AccountCard, AccountCardFooter, AccountCardBody } from "./AccountCard";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useToast } from "@/components/ui/use-toast";
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+
+export default function UpdateNameCard({ name }: { name: string }) {
+ const { toast } = useToast();
+ const [isPending, startTransition] = useTransition();
+ const router = useRouter();
+ const handleSubmit = async (event: React.SyntheticEvent) => {
+ event.preventDefault();
+ const target = event.target as HTMLFormElement;
+ const form = new FormData(target);
+ const { name } = Object.fromEntries(form.entries()) as { name: string };
+ if (name.length < 3) {
+ toast({
+ description: "Name must be longer than 3 characters.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ startTransition(async () => {
+ const res = await fetch("/api/account", {
+ method: "PUT",
+ body: JSON.stringify({ name }),
+ headers: { "Content-Type": "application/json" },
+ });
+ if (res.status === 200)
+ toast({ description: "Successfully updated name!" });
+ router.refresh();
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/account/UserSettings.tsx b/src/app/account/UserSettings.tsx
new file mode 100644
index 0000000..9d38a40
--- /dev/null
+++ b/src/app/account/UserSettings.tsx
@@ -0,0 +1,17 @@
+"use client";
+import UpdateNameCard from "./UpdateNameCard";
+import UpdateEmailCard from "./UpdateEmailCard";
+import { AuthSession } from "@/lib/auth/utils";
+
+export default function UserSettings({
+ session,
+}: {
+ session: AuthSession["session"];
+}) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx
new file mode 100644
index 0000000..0588287
--- /dev/null
+++ b/src/app/account/page.tsx
@@ -0,0 +1,16 @@
+import UserSettings from "./UserSettings";
+import { checkAuth, getUserAuth } from "@/lib/auth/utils";
+
+export default async function Account() {
+ await checkAuth();
+ const { session } = await getUserAuth();
+
+ return (
+
+ Account
+
+
+
+
+ );
+}
diff --git a/src/app/api/account/route.ts b/src/app/api/account/route.ts
new file mode 100644
index 0000000..4f16cf9
--- /dev/null
+++ b/src/app/api/account/route.ts
@@ -0,0 +1,15 @@
+import { getUserAuth } from "@/lib/auth/utils";
+import { db } from "@/server/db/index";
+import { users } from "@/server/db/schema/auth";
+import { eq } from "drizzle-orm";
+import { revalidatePath } from "next/cache";
+
+export async function PUT(request: Request) {
+ const { session } = await getUserAuth();
+ if (!session) return new Response("Error", { status: 400 });
+ const body = (await request.json()) as { name?: string; email?: string };
+
+ await db.update(users).set({ ...body }).where(eq(users.id, session.user.id));
+ revalidatePath("/account");
+ return new Response(JSON.stringify({ message: "ok" }), { status: 200 });
+}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 1570f88..f9f233c 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -1,7 +1,14 @@
-import NextAuth from "next-auth";
+import { DefaultSession } from "next-auth";
+import NextAuth from "next-auth/next";
+import { authOptions } from "@/lib/auth/utils";
-import { authOptions } from "@/server/auth";
+declare module "next-auth" {
+ interface Session {
+ user: DefaultSession["user"] & {
+ id: string;
+ };
+ }
+}
-// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
diff --git a/src/app/api/child/register/route.ts b/src/app/api/child/register/route.ts
deleted file mode 100644
index 8383880..0000000
--- a/src/app/api/child/register/route.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export async function POST(req: Request) {
- const body = await req.json();
- console.log("route", "register", body);
-}
diff --git a/src/app/api/child/route.ts b/src/app/api/child/route.ts
deleted file mode 100644
index 014d00e..0000000
--- a/src/app/api/child/route.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { db } from "@/server/db";
-import { child } from "@/server/db/schema";
-import { StatusCodes, getReasonPhrase } from "http-status-codes";
-import { NextResponse } from "next/server";
-import { eq } from "drizzle-orm";
-import { getServerAuthSession } from "@/server/auth";
-
-export async function GET(request: Request) {
- const session = await getServerAuthSession();
- if (!session || !session.user)
- return NextResponse.json(
- { error: getReasonPhrase(StatusCodes.UNAUTHORIZED) },
- { status: StatusCodes.UNAUTHORIZED },
- );
- const activeChildren = await db.query.child.findMany({
- where: eq(child.parentId, session.user.id),
- with: {
- devices: {
- with: { pings: true },
- },
- },
- });
-
- return NextResponse.json(activeChildren);
-}
diff --git a/src/app/api/device/connect/route.ts b/src/app/api/device/connect/route.ts
deleted file mode 100644
index 353c95a..0000000
--- a/src/app/api/device/connect/route.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { db } from '@/server/db';
-import { eq } from 'drizzle-orm';
-import { StatusCodes } from 'http-status-codes';
-import { child, device } from '@/server/db/schema';
-import { createApiKey } from '@/lib/services/auth/api';
-import { badRequest } from '@/app/api/responses';
-
-type DeviceConnectRequest = {
- deviceId: string;
- childId: string;
- deviceName: string;
-}
-const POST = async (req: Request, res: Response) => {
- if (req.method === 'POST') {
- const { deviceId, childId, deviceName } = await req.json() as DeviceConnectRequest;
- console.log('route', 'childId', childId);
- console.log('route', 'deviceId', deviceId);
-
- if (!childId || !deviceId) {
- return badRequest('Invalid registration request');
- }
-
- const childToRegister = (
- await db.selectDistinct().from(child).where(eq(child.id, childId))
- )[0];
-
- if (!childToRegister) {
- return badRequest('Invalid registration request');
- }
-
- let done = false;
- let pin = 2021;
- while (!done) {
- pin = Math.floor(1000 + Math.random() * 9000);
- console.log('route', 'device/connect', 'checking for PIN', pin);
- const exists = await db
- .selectDistinct()
- .from(device)
- .where(eq(device.pin, pin));
- console.log('route', 'exists', exists);
- done = exists.length === 0;
- }
-
- const apiKey = createApiKey();
- await db
- .insert(device)
- .values({
- childId: childToRegister.id,
- deviceId: deviceId,
- deviceName: deviceName,
- pin,
- apiKey: apiKey,
- })
- .execute();
- return Response.json(
- { childId, deviceId, deviceName, pin, apiKey },
- { status: StatusCodes.CREATED },
- );
- }
- return badRequest('Invalid registration request');
-};
-
-export { POST };
diff --git a/src/app/api/responses/index.ts b/src/app/api/responses/index.ts
deleted file mode 100644
index 5cd6bad..0000000
--- a/src/app/api/responses/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { StatusCodes } from 'http-status-codes';
-
-export const notAuthorised = () =>
- new Response('Not authorised', {
- status: StatusCodes.BAD_REQUEST,
- });
-export const badRequest = (message: string) =>
- new Response(message, {
- status: StatusCodes.BAD_REQUEST,
- });
diff --git a/src/app/debug/page.tsx b/src/app/debug/page.tsx
new file mode 100644
index 0000000..67335b0
--- /dev/null
+++ b/src/app/debug/page.tsx
@@ -0,0 +1,12 @@
+import { Button } from "@/components/ui/button";
+import React from "react";
+
+const DebugPage = () => {
+ return (
+
+
+
+ );
+};
+
+export default DebugPage;
diff --git a/src/app/devices/page.tsx b/src/app/devices/page.tsx
new file mode 100644
index 0000000..7003893
--- /dev/null
+++ b/src/app/devices/page.tsx
@@ -0,0 +1,19 @@
+import DeviceList from "@/components/devices/DeviceList";
+import NewDeviceModal from "@/components/devices/DeviceModal";
+import { api } from "@/trpc/server";
+import { checkAuth } from "@/lib/auth/utils";
+
+export default async function Devices() {
+ await checkAuth();
+ const { devices } = await api.devices.getDevices.query();
+
+ return (
+
+
+
Devices
+
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 835e5d3..e096f50 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,26 +1,21 @@
import "@/styles/globals.css";
-import { Analytics } from "@vercel/analytics/react";
-import { ABeeZee as TheFont } from "next/font/google";
-import { cookies } from "next/headers";
+
+import { Inter } from "next/font/google";
import { TRPCReactProvider } from "@/trpc/react";
-import { ThemeProvider } from "@/components/providers/theme-provider";
-import { type Metadata } from "next";
-import NextAuthProvider from "@/lib/services/auth/provider";
+import { ThemeProvider } from "@/components/ThemeProvider";
+import { Toaster } from "@/components/ui/toaster";
+import NextAuthProvider from "@/lib/auth/Provider";
-const inter = TheFont({
- weight: "400",
+const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
-export const metadata: Metadata = {
+export const metadata = {
title: "Kidarr",
- description: "Radar for your kids",
- manifest: "/site.webmanifest",
- icons: {
- icon: "/favicon.ico",
- },
+ description: "Radarr for your kids",
+ icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default function RootLayout({
@@ -29,16 +24,20 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
+
-
-
-
- {children}
-
-
-
-
+
+
+ {children}
+
+
+
+
);
diff --git a/src/app/loading.tsx b/src/app/loading.tsx
new file mode 100644
index 0000000..568eb6e
--- /dev/null
+++ b/src/app/loading.tsx
@@ -0,0 +1,25 @@
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3a2fa6e..6c5d549 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,12 +1,17 @@
-import { getServerAuthSession } from '@/server/auth';
-import HomePage from '@/components/pages/home-page';
-import { redirect } from 'next/navigation';
+
+import HomePage from "@/components/pages/home-page";
+import { getUserAuth } from "@/lib/auth/utils";
+import { redirect } from "next/navigation";
export default async function Home() {
- const session = await getServerAuthSession();
-
- if (session?.user) {
- redirect('/dashboard');
+ const { session } = await getUserAuth();
+ if (session) {
+ redirect("/dashboard");
}
- return ;
+
+ return (
+
+
+
+ );
}
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
new file mode 100644
index 0000000..a801d9e
--- /dev/null
+++ b/src/app/settings/page.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { useTheme } from "next-themes";
+
+export default function Page() {
+ const { setTheme } = useTheme();
+ return (
+
+
Settings
+
+
+
Appearance
+
+ Customize the appearance of the app. Automatically switch between
+ day and night themes.
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
new file mode 100644
index 0000000..26b5257
--- /dev/null
+++ b/src/components/Navbar.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import { usePathname } from "next/navigation";
+
+import { Button } from "@/components/ui/button";
+
+import { AlignRight } from "lucide-react";
+import { defaultLinks } from "@/config/nav";
+
+export default function Navbar() {
+ const [open, setOpen] = useState(false);
+ const pathname = usePathname();
+ return (
+
+
+ {open ? (
+
+
+ {defaultLinks.map((link) => (
+ - setOpen(false)} className="">
+
+ {link.title}
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
new file mode 100644
index 0000000..8d1d63f
--- /dev/null
+++ b/src/components/Sidebar.tsx
@@ -0,0 +1,55 @@
+import Link from "next/link";
+
+import SidebarItems from "./SidebarItems";
+import { Avatar, AvatarFallback } from "./ui/avatar";
+
+import { AuthSession, getUserAuth } from "@/lib/auth/utils";
+
+const Sidebar = async () => {
+ const session = await getUserAuth();
+ if (session.session === null) return null;
+
+ return (
+
+ );
+};
+
+export default Sidebar;
+
+const UserDetails = ({ session }: { session: AuthSession }) => {
+ if (session.session === null) return null;
+ const { user } = session.session;
+
+ if (!user?.name || user.name.length == 0) return null;
+
+ return (
+
+
+
+
{user.name ?? "John Doe"}
+
+ {user.email ?? "john@doe.com"}
+
+
+
+
+ {user.name
+ ? user.name
+ ?.split(" ")
+ .map((word) => word[0].toUpperCase())
+ .join("")
+ : "~"}
+
+
+
+
+ );
+};
diff --git a/src/components/SidebarItems.tsx b/src/components/SidebarItems.tsx
new file mode 100644
index 0000000..2569741
--- /dev/null
+++ b/src/components/SidebarItems.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { LucideIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { defaultLinks, additionalLinks } from "@/config/nav";
+
+export interface SidebarLink {
+ title: string;
+ href: string;
+ icon: LucideIcon;
+}
+
+const SidebarItems = () => {
+ return (
+ <>
+
+ {additionalLinks.length > 0
+ ? additionalLinks.map((l) => (
+
+ ))
+ : null}
+ >
+ );
+};
+export default SidebarItems;
+
+const SidebarLinkGroup = ({
+ links,
+ title,
+ border,
+}: {
+ links: SidebarLink[];
+ title?: string;
+ border?: boolean;
+}) => {
+ const pathname = usePathname();
+
+ return (
+
+ {title ? (
+
+ {title}
+
+ ) : null}
+
+ {links.map((link) => (
+ -
+
+
+ ))}
+
+
+ );
+};
+const SidebarLink = ({
+ link,
+ active,
+}: {
+ link: SidebarLink;
+ active: boolean;
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx
new file mode 100644
index 0000000..b0ff266
--- /dev/null
+++ b/src/components/ThemeProvider.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import * as React from "react";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { type ThemeProviderProps } from "next-themes/dist/types";
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children};
+}
diff --git a/src/components/children/child-form.tsx b/src/components/children/child-form.tsx
new file mode 100644
index 0000000..8f015c4
--- /dev/null
+++ b/src/components/children/child-form.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import {
+ type Child,
+ type NewChildParams,
+ insertChildParams,
+} from "@/server/db/schema/children";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { api as trpc } from "@/trpc/react";
+import { Button } from "@/components/ui/button";
+import { type z } from "zod";
+import { useRouter } from "next/navigation";
+import { useToast } from "@/components/ui/use-toast";
+
+const ChildForm = ({
+ child,
+ closeModal,
+}: {
+ child?: Child;
+ closeModal?: () => void;
+}) => {
+ const { toast } = useToast();
+
+ const editing = !!child?.id;
+
+ const router = useRouter();
+ const utils = trpc.useContext();
+
+ const form = useForm>({
+ // latest Zod release has introduced a TS error with zodResolver
+ // open issue: https://github.com/colinhacks/zod/issues/2663
+ // errors locally but not in production
+ resolver: zodResolver(insertChildParams),
+ defaultValues: child ?? {
+ name: "",
+ email: "",
+ avatar: "",
+ },
+ });
+
+ const onSuccess = async (
+ action: "create" | "update" | "delete",
+ data?: { error?: string },
+ ) => {
+ if (data?.error) {
+ toast({
+ title: `${action
+ .slice(0, 1)
+ .toUpperCase()
+ .concat(action.slice(1))} Failed`,
+ description: data.error,
+ variant: "destructive",
+ });
+ return;
+ }
+
+ await utils.children.getChildren.invalidate();
+ router.refresh();
+ if (closeModal) closeModal();
+ toast({
+ title: "Success",
+ description: `Child ${action}d!`,
+ variant: "default",
+ });
+ };
+
+ const { mutate: createChild, isLoading: isCreating } =
+ trpc.children.createChild.useMutation({
+ onSuccess: (res) => onSuccess("create"),
+ });
+
+ const { mutate: updateChild, isLoading: isUpdating } =
+ trpc.children.updateChild.useMutation({
+ onSuccess: (res) => onSuccess("update"),
+ });
+
+ const { mutate: deleteChild, isLoading: isDeleting } =
+ trpc.children.deleteChild.useMutation({
+ onSuccess: (res) => onSuccess("delete"),
+ });
+
+ const handleSubmit = (values: NewChildParams) => {
+ if (editing) {
+ updateChild({ ...values, id: child.id });
+ } else {
+ createChild(values);
+ }
+ };
+ return (
+
+
+ );
+};
+
+export default ChildForm;
diff --git a/src/components/children/child-list.tsx b/src/components/children/child-list.tsx
new file mode 100644
index 0000000..8ce3fbf
--- /dev/null
+++ b/src/components/children/child-list.tsx
@@ -0,0 +1,73 @@
+"use client";
+import { type CompleteChild } from "@/server/db/schema/children";
+import { api as trpc } from "@/trpc/react";
+import ChildModal from "./child-modal";
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import { Icons } from "@/components/icons";
+import ConnectDeviceDialog from "@/components/children/connect-device-dialog";
+
+export default function ChildList({ children }: { children: CompleteChild[] }) {
+ const { data: c } = trpc.children.getChildren.useQuery(undefined, {
+ initialData: { children },
+ refetchOnMount: false,
+ });
+
+ if (c.children.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+ Name
+ Last seen at
+ Actions
+
+
+
+ {c.children?.map((kid) => )}
+
+
+ );
+}
+
+const Child = ({ child }: { child: CompleteChild }) => {
+ return (
+
+ {child.name}
+ Douglas
+
+
+
+
+
+
+
+ );
+};
+
+const EmptyState = () => {
+ return (
+
+
+ No children
+
+
+ Get started by creating a new child.
+
+
+
+
+
+ );
+};
diff --git a/src/components/children/child-modal.tsx b/src/components/children/child-modal.tsx
new file mode 100644
index 0000000..e93f306
--- /dev/null
+++ b/src/components/children/child-modal.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "../ui/dialog";
+import ChildForm from "./child-form";
+import { type Child } from "@/server/db/schema/children";
+import { Icons } from "../icons";
+
+export default function ChildModal({
+ child,
+ emptyState,
+}: {
+ child?: Child;
+ emptyState?: boolean;
+}) {
+ const [open, setOpen] = useState(false);
+ const closeModal = () => setOpen(false);
+ const editing = !!child?.id;
+ return (
+
+ );
+}
diff --git a/src/components/children/child-select-list.tsx b/src/components/children/child-select-list.tsx
index f123c8c..00baa53 100644
--- a/src/components/children/child-select-list.tsx
+++ b/src/components/children/child-select-list.tsx
@@ -1,5 +1,5 @@
-'use client';
-import React from 'react';
+"use client";
+import React from "react";
import {
Select,
SelectContent,
@@ -7,15 +7,15 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select';
-import type ChildModel from '@/lib/models/child';
+} from "@/components/ui/select";
+import { type Child } from "@/server/db/schema/children";
type ChildSelectListProps = {
- kids: ChildModel[];
-}
-const ChildSelectList: React.FC = ({ kids }) => {
+ children: Child[];
+};
+const ChildSelectList: React.FC = ({ children: kids }) => {
return (
-