All current db functions are written. Just need to make api routes then we are done
This commit is contained in:
commit
f734551d3a
17
.env.example
Normal file
17
.env.example
Normal 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
61
.eslintrc.cjs
Normal 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
46
.gitignore
vendored
Normal 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
29
README.md
Normal 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
11
drizzle.config.ts
Normal 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
10
next.config.js
Normal 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
47
package.json
Normal 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
4109
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
postcss.config.cjs
Normal file
7
postcss.config.cjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
6
prettier.config.js
Normal file
6
prettier.config.js
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
20
src/app/layout.tsx
Normal file
20
src/app/layout.tsx
Normal 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
37
src/app/page.tsx
Normal 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
44
src/env.js
Normal 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
18
src/server/db/index.ts
Normal 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
163
src/server/db/schema.ts
Normal 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
306
src/server/functions.ts
Normal 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
150
src/server/types.ts
Normal 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
3
src/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
14
tailwind.config.ts
Normal file
14
tailwind.config.ts
Normal 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
42
tsconfig.json
Normal 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"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user