All current db functions are written. Just need to make api routes then we are done

This commit is contained in:
Gabriel Brown 2024-10-16 16:51:08 -05:00
commit f734551d3a
21 changed files with 5140 additions and 0 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.js"
# should be updated accordingly.
# Drizzle
DATABASE_URL="postgresql://postgres:password@localhost:5432/wavelength_server"
# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"

61
.eslintrc.cjs Normal file
View File

@ -0,0 +1,61 @@
/** @type {import("eslint").Linter.Config} */
const config = {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
},
"plugins": [
"@typescript-eslint",
"drizzle"
],
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": {
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": {
"attributes": false
}
}
],
"drizzle/enforce-delete-with-where": [
"error",
{
"drizzleObjectName": [
"db",
"ctx.db"
]
}
],
"drizzle/enforce-update-with-where": [
"error",
{
"drizzleObjectName": [
"db",
"ctx.db"
]
}
]
}
}
module.exports = config;

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
db.sqlite
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# idea files
.idea

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# Create T3 App
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`.
## What's next? How do I make an app with this?
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.
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.
- [Next.js](https://nextjs.org)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Drizzle](https://orm.drizzle.team)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [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
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## How do I deploy this?
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.

11
drizzle.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { type Config } from "drizzle-kit";
import { env } from "~/env";
export default {
schema: "./src/server/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: env.DATABASE_URL,
}
} satisfies Config;

10
next.config.js Normal file
View File

@ -0,0 +1,10 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.js");
/** @type {import("next").NextConfig} */
const config = {};
export default config;

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "wavelength_server",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "next dev",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@t3-oss/env-nextjs": "^0.10.1",
"drizzle-orm": "^0.33.0",
"geist": "^1.3.0",
"next": "^14.2.4",
"postgres": "^3.4.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zod": "^3.23.3"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"drizzle-kit": "^0.24.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.4",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.5.3"
},
"ct3aMetadata": {
"initVersion": "7.37.0"
},
"packageManager": "pnpm@9.12.1"
}

4109
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
postcss.config.cjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
tailwindcss: {},
},
};
module.exports = config;

6
prettier.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
plugins: ["prettier-plugin-tailwindcss"],
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

20
src/app/layout.tsx Normal file
View File

@ -0,0 +1,20 @@
import "~/styles/globals.css";
import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${GeistSans.variable}`}>
<body>{children}</body>
</html>
);
}

37
src/app/page.tsx Normal file
View File

@ -0,0 +1,37 @@
import Link from "next/link";
export default function HomePage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-[5rem]">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className="text-2xl font-bold">First Steps </h3>
<div className="text-lg">
Just the basics - Everything you need to know to set up your
database and authentication.
</div>
</Link>
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 text-white hover:bg-white/20"
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className="text-2xl font-bold">Documentation </h3>
<div className="text-lg">
Learn more about Create T3 App, the libraries it uses, and how to
deploy it.
</div>
</Link>
</div>
</div>
</main>
);
}

44
src/env.js Normal file
View File

@ -0,0 +1,44 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
DATABASE_URL: z.string().url(),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
},
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
* `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
});

18
src/server/db/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { env } from "~/env";
import * as schema from "./schema";
/**
* Cache the database connection in development. This avoids creating a new connection on every HMR
* update.
*/
const globalForDb = globalThis as unknown as {
conn: postgres.Sql | undefined;
};
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema });

163
src/server/db/schema.ts Normal file
View File

@ -0,0 +1,163 @@
// https://orm.drizzle.team/docs/sql-schema-declaration
import { sql } from "drizzle-orm";
import {
boolean,
index,
integer,
jsonb,
numeric,
pgEnum,
pgTable,
serial,
text,
timestamp,
varchar,
} from "drizzle-orm/pg-core";
export const users = pgTable(
'users',
{
id: serial('id').primaryKey(),
appleId: varchar('apple_id', { length: 200 }).unique(),
email: varchar('email', { length: 100 }).unique().notNull(),
fullName: varchar('full_name', { length: 100 }).notNull(),
pfpUrl: varchar('pfp_url', { length: 255 }),
pushToken: varchar('push_token', { length: 100 }).unique().notNull(),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`).notNull(),
metadata: jsonb('metadata'),
},
(table) => ({
appleIdIndex: index('apple_id_idx').on(table.appleId),
emailIndex: index('email_idx').on(table.email),
fullNameIndex: index('full_name_idx').on(table.fullName),
})
);
export const relationships = pgTable(
'relationships',
{
id: serial('id').primaryKey(),
title: varchar('title', { length: 50 })
.default("My Relationship").notNull(),
requestorId: integer('requestor_id').references(() => users.id).notNull(),
isAccepted: boolean('is_accepted').default(false),
relationshipStartDate: timestamp('relationship_start_date', { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`).notNull(),
},
(table) => ({
requestorIdIndex: index('requestor_id_idx').on(table.requestorId),
})
);
export const userRelationships = pgTable(
'user_relationships',
{
id: serial('id').primaryKey(),
userId: integer('user_id').references(() => users.id).notNull(),
relationshipId: integer('relationship_id').references(() => relationships.id).notNull(),
},
(table) => ({
userIdIndex: index('user_id_idx').on(table.userId),
relationshipIdIndex: index('relationship_id_idx').on(table.relationshipId),
})
);
export const countdowns = pgTable(
'countdowns',
{
id: serial('id').primaryKey(),
relationshipId: integer('relationship_id').references(() => relationships.id).notNull(),
title: varchar('title', { length: 50 })
.default('Countdown to Next Visit').notNull(),
date: timestamp('date', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`).notNull(),
},
(table) => ({
relationshipIdIndex: index('relationship_id_idx').on(table.relationshipId),
})
);
export const messages = pgTable(
'messages',
{
id: serial('id').primaryKey(),
senderId: integer('sender_id').references(() => users.id).notNull(),
receiverId: integer('receiver_id').references(() => users.id).notNull(),
text: text('text').notNull(),
createdAt: timestamp('created_at', { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`).notNull(),
isRead: boolean('is_read').default(false),
hasLocation: boolean('has_location').default(false),
hasMedia: boolean('has_media').default(false),
hasQuickReply: boolean('has_quick_reply').default(false),
},
(table) => ({
senderIdIndex: index('sender_id_idx').on(table.senderId),
receiverIdIndex: index('receiver_id_idx').on(table.receiverId),
})
);
export const mediaTypes = pgEnum(
'message_media_types',
['image', 'video', 'audio', 'file']
);
export const media = pgTable(
'media',
{
id: serial('id').primaryKey(),
messageId: integer('message_id').references(() => messages.id).notNull(),
type: mediaTypes('type').notNull(),
url: varchar('url', { length: 255 }).notNull(),
size: numeric('size'),
metadata: varchar('metadata', { length: 255 }),
order: integer('order'),
},
(table) => ({
messageIdIndex: index('message_id_idx').on(table.messageId),
})
);
export const locations = pgTable(
'locations',
{
id: serial('id').primaryKey(),
messageId: integer('message_id').references(() => messages.id).notNull(),
latitude: numeric('latitude').notNull(),
longitude: numeric('longitude').notNull(),
},
(table) => ({
messageIdIndex: index('message_id_idx').on(table.messageId),
})
);
export const quickReplyType = pgEnum(
'quick_reply_types',
['radio', 'checkbox']
);
export const quickReplies = pgTable(
'quick_replies',
{
id: serial('id').primaryKey(),
messageId: integer('message_id').references(() => messages.id).notNull(),
type: quickReplyType('type').notNull(),
keepIt: boolean('keep_it').default(false),
},
(table) => ({
messageIdIndex: index('message_id_idx').on(table.messageId),
})
);
export const quickReplyOptions = pgTable(
'quick_reply_options',
{
id: serial('id').primaryKey(),
quickReplyId: integer('quick_reply_id').references(() => quickReplies.id).notNull(),
title: varchar('title', { length: 100 }).notNull(),
value: varchar('value', { length: 100 }).notNull(),
},
(table) => ({
quickReplyIdIndex: index('quick_reply_id_idx').on(table.quickReplyId),
})
);

306
src/server/functions.ts Normal file
View File

@ -0,0 +1,306 @@
import 'server-only';
import { db } from '~/server/db';
import * as schema from '~/server/db/schema';
import { eq, and, or, like, not } from 'drizzle-orm';
import { User,
Relationship,
UserRelationship,
RelationshipData,
Countdown,
InitialData,
Message,
MessageMedia,
MessageLocation,
QuickReply,
QuickReplyOption,
} from '~/server/types';
export const getUser = async (userId: number) => {
try {
const users = await db.select().from(schema.users)
.where(eq(schema.users.id, userId))
return (users.length > 0) ? users[0] as User : null;
} catch (error) {
console.error(error);
return null;
}
};
export const getInitialDataByAppleId = async (appleId: string) => {
try {
const users = await db.select().from(schema.users)
.where(eq(schema.users.appleId, appleId))
if (users.length === 0) return null;
const user = users[0] as User;
const userRelationships = await db.select()
.from(schema.userRelationships)
.where(eq(schema.userRelationships.userId, user.id))
let relationshipData: RelationshipData | undefined;
let countdown: Countdown | undefined;
if (userRelationships.length > 0) {
const userRelationship = userRelationships[0] as UserRelationship;
const relationships = await db.select()
.from(schema.relationships)
.where(eq(schema.relationships.id, userRelationship.relationshipId))
if (relationships.length > 0) {
const relationship = relationships[0] as Relationship;
const partners = await db.select()
.from(schema.users)
.innerJoin(schema.userRelationships,
eq(schema.users.id, schema.userRelationships.userId))
.where(
and(
eq(schema.userRelationships.relationshipId, relationship.id),
not(eq(schema.userRelationships.userId, user.id))
)
);
if (partners.length > 0) {
const partner = partners[0]?.users as User;
relationshipData = {
relationship,
partner,
};
const countdowns = await db.select()
.from(schema.countdowns)
.where(eq(schema.countdowns.relationshipId, relationship.id))
.orderBy(schema.countdowns.date)
.limit(1);
if (countdowns.length > 0) {
countdown = countdowns[0] as Countdown;
}
}
}
}
const initialData: InitialData = {
user,
relationshipData,
countdown,
};
return initialData;
} catch (error) {
console.error(error);
return null;
}
};
export const createUser = async (
appleId: string, email: string,
fullName: string, pushToken: string
) => {
try {
if (!appleId || !email || !fullName || !pushToken) {
throw new Error("Error: All required fields must be filled");
}
// Check if username or email is already taken
const existingUser = await db.select().from(schema.users)
.where(or(eq(schema.users.appleId, appleId), eq(schema.users.email, email)));
if (existingUser.length > 0) {
throw new Error("Username or email is already in use");
}
const newUsers: User[] = await db.insert(schema.users).values({
appleId, email, fullName, pushToken
}).returning() as User[]; // return the newly created user
if (!newUsers.length || !newUsers[0]?.id)
throw new Error("Failed to create new user");
return newUsers[0];
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create new user: ${error.message}`);
} else {
throw new Error("Unknown error occurred while creating new user");
}
}
};
export const updatePushToken = async (userId: number, pushToken: string): Promise<boolean> => {
try {
const result = await db.update(schema.users)
.set({ pushToken: pushToken })
.where(
and(
eq(schema.users.id, userId),
not(eq(schema.users.pushToken, pushToken))
)
)
.returning({ updatedId: schema.users.id });
return result.length > 0;
} catch (error) {
console.error('Error updating push token:', error);
return false;
}
};
export const updatePfpUrl = async (userId: number, pfpUrl: string) => {
try {
const result = await db.update(schema.users)
.set({ pfpUrl: pfpUrl })
.where(eq(schema.users.id, userId))
.returning({ updatedId: schema.users.id });
return result.length > 0;
} catch (error) {
console.error('Error updating pfp url:', error);
}
};
export const checkRelationshipStatus = async (userId: number): Promise<RelationshipData> => {
try {
const user = await getUser(userId);
if (!user) throw new Error("User not found");
const userRelationship = await db.select()
.from(schema.userRelationships)
.where(eq(schema.userRelationships.userId, user.id))
.limit(1)
.then(results => results[0]);
if (!userRelationship) throw new Error('No relationships found for user');
const relationship = await db.select()
.from(schema.relationships)
.where(eq(schema.relationships.id, userRelationship.relationshipId))
.limit(1)
.then(results => results[0] as Relationship);
if (!relationship) throw new Error('Relationship not found');
const partner = await db.select()
.from(schema.users)
.innerJoin(schema.userRelationships,
eq(schema.users.id, schema.userRelationships.userId))
.where(
and(
eq(schema.userRelationships.relationshipId, relationship.id),
not(eq(schema.userRelationships.userId, user.id))
)
)
.limit(1)
.then(results => results[0]?.users as User);
if (!partner) throw new Error('No partner found for relationship');
return { relationship, partner };
} catch (error) {
console.error('Error checking relationship status:', error);
throw error; // Re-throw the error to be handled by the caller
}
};
export const updateRelationshipStatus = async (
userId: number, status: 'accepted' | 'rejected'
) => {
const users = await db.select().from(schema.users)
.where(eq(schema.users.id, userId));
const user = users[0] as User;
if (!user) throw new Error("User not found");
const userRelationships = await db.select()
.from(schema.userRelationships)
.where(eq(schema.userRelationships.userId, user.id));
if (userRelationships.length === 0) {
throw new Error('No relationships found for user');
}
const userRelationship = userRelationships[0] as UserRelationship;
const relationships = await db.select()
.from(schema.relationships)
.where(eq(schema.relationships.id, userRelationship.relationshipId));
if (relationships.length === 0) {
throw new Error('Relationship not found');
}
const relationship = relationships[0] as Relationship;
if (status === 'accepted') {
await db.update(schema.relationships)
.set({ isAccepted: true })
.where(eq(schema.relationships.id, relationship.id));
const partners = await db.select()
.from(schema.users)
.innerJoin(schema.userRelationships,
eq(schema.users.id, schema.userRelationships.userId))
.where(
and(
eq(schema.userRelationships.relationshipId, relationship.id),
not(eq(schema.userRelationships.userId, user.id))
)
);
if (partners.length === 0) {
throw new Error('No partners found for relationship');
}
const partner = partners[0]?.users as User;
const relationshipData: RelationshipData = {
relationship,
partner,
};
return relationshipData;
} else if (status === 'rejected') {
await db.delete(schema.userRelationships)
.where(eq(schema.userRelationships.id, userRelationship.id));
await db.delete(schema.relationships)
.where(eq(schema.relationships.id, relationship.id));
return null;
}
};
export const searchUsers = async (userId: number, searchTerm: string) => {
try {
const users = await db.select().from(schema.users)
.where(
and(
or(
like(schema.users.fullName, `%${searchTerm}%`),
like(schema.users.email, `%${searchTerm}%`)
),
not(eq(schema.users.id, userId))
)
);
if (users.length === 0) throw new Error("No users found");
return users as User[];
} catch (error) {
console.error('Error searching users:', error);
}
};
export const createRelationshipRequest = async (userId: number, partnerId: number) => {
try {
const user = await getUser(userId);
if (!user) throw new Error("User not found");
const partner = await getUser(partnerId);
if (!partner) throw new Error("Partner not found");
const existingRelationship = await db.select({
relationshipId: schema.userRelationships.relationshipId,
status: schema.relationships.isAccepted,
})
.from(schema.userRelationships)
.innerJoin(
schema.relationships,
eq(schema.userRelationships.relationshipId, schema.relationships.id)
)
.where(
or(
eq(schema.userRelationships.userId, user.id),
eq(schema.userRelationships.userId, partner.id)
)
).limit(1);
if (existingRelationship.length > 0) {
throw new Error("Relationship already exists");
}
const newRelationship = await db.insert(schema.relationships).values({
requestorId: user.id,
}).returning() as Relationship[];
if (!newRelationship.length || !newRelationship[0]?.id)
throw new Error("Failed to create new relationship");
const relationship = newRelationship[0];
await db.insert(schema.userRelationships).values([
{ userId: userId, relationshipId: relationship.id },
{ userId: partnerId, relationshipId: relationship.id },
]);
return { relationship, partner };
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(`Failed to create new relationship: ${error.message}`);
} else {
throw new Error("Unknown error occurred while creating new relationship");
}
}
};

150
src/server/types.ts Normal file
View File

@ -0,0 +1,150 @@
/* Types */
// User Table in DB
export type User = {
id: number;
appleId: string | null;
email: string;
fullName: string;
pfpUrl: string | null;
pushToken: string;
createdAt: Date;
metadata?: Record<string, string>;
};
// Relationship Table in DB
export type Relationship = {
id: number;
title: string;
requestorId: number;
isAccepted: boolean;
relationshipStartDate: Date;
};
export type UserRelationship = {
id: number;
userId: number;
relationshipId: number;
};
// Mutated Data from Relationship
// & UserRelationship Tables in DB
export type RelationshipData = {
relationship: Relationship;
partner: User;
};
// Countdown Table in DB
export type Countdown = {
id: number;
relationshipId: number;
title: string;
date: Date;
createdAt: Date;
};
// Mutated Data for Login
// API Response
export type InitialData = {
user: User;
relationshipData?: RelationshipData;
countdown?: Countdown;
};
// Message Table in DB
export type Message = {
id: number;
senderId: number;
receiverId: number;
text: string;
createdAt: Date;
isRead: boolean;
hasLocation: boolean;
hasMedia: boolean;
hasQuickReply: boolean;
};
// MessageMedia Table in DB
export type MessageMedia = {
id: number;
messageId: number;
mediaType:
'image' | 'video' | 'audio' | 'file';
url: string;
size?: number;
metadata?: string;
order: number;
};
// MessageLocation Table in DB
export type MessageLocation = {
id: number;
messageId: number;
latitude: number;
longitude: number;
};
// Quick Reply Table in DB
export type QuickReply = {
id: number;
messageId: number;
type: 'radio' | 'checkbox';
keepIt: boolean;
};
// Quick Reply Option Table in DB
export type QuickReplyOption = {
id: number;
quickReplyId: number;
title: string;
value: string;
};
export type GCUser = {
_id: number;
name: string;
avatar?: string;
};
export type GCQuickReplies = {
type: 'radio' | 'checkbox';
values: GCQuickReplyOptions[];
keepIt?: boolean;
};
export type GCQuickReplyOptions = {
title: string;
value: string;
};
export type GCLocation = {
latitude: number;
longitude: number;
};
export type GCMessage = {
_id: number;
text: string;
createdAt: Date;
user: GCUser;
image?: string;
video?: string;
audio?: string;
location?: GCLocation;
system?: boolean;
sent?: boolean;
received?: boolean;
pending?: boolean;
quickReplies?: GCQuickReplies;
};
export type GCState = {
messages: any[];
step: number;
loadEarlier?: boolean;
isLoadingEarlier?: boolean;
isTyping: boolean;
};
export enum ActionKind {
SEND_MESSAGE = 'SEND_MESSAGE',
LOAD_EARLIER_MESSAGES = 'LOAD_EARLIER_MESSAGES',
LOAD_EARLIER_START = 'LOAD_EARLIER_START',
SET_IS_TYPING = 'SET_IS_TYPING',
// LOAD_EARLIER_END = 'LOAD_EARLIER_END',
};
export type GCStateAction = {
type: ActionKind;
payload?: any;
};
export type NotificationMessage = {
sound?: string;
title: string;
body: string;
data?: any;
};

3
src/styles/globals.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
tailwind.config.ts Normal file
View File

@ -0,0 +1,14 @@
import { type Config } from "tailwindcss";
import { fontFamily } from "tailwindcss/defaultTheme";
export default {
content: ["./src/**/*.tsx"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
},
},
},
plugins: [],
} satisfies Config;

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"checkJs": true,
/* Bundled projects */
"lib": ["dom", "dom.iterable", "ES2022"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"plugins": [{ "name": "next" }],
"incremental": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
},
"include": [
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.js",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}