mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1136c3f767 | ||
|
|
42b496cd77 | ||
|
|
4acb5ee020 | ||
|
|
ea18781cc6 | ||
|
|
593617c0ff | ||
|
|
c6a139d88d | ||
|
|
b7357a4546 | ||
|
|
5eac959d15 | ||
|
|
74c86ecfbe | ||
|
|
f353e590e1 | ||
|
|
a4cc3e10c2 | ||
|
|
7321f56ee2 | ||
|
|
8800d9adc6 | ||
|
|
22cd535527 | ||
|
|
1d0e9592df | ||
|
|
2ef4af0ff2 | ||
|
|
542a6b1592 | ||
|
|
613ef94dba | ||
|
|
1dc2a1fadf | ||
|
|
41a606f5c1 | ||
|
|
7b2b9855f9 | ||
|
|
b2b519ba2e | ||
|
|
5cf89392ff | ||
|
|
0f05304ec3 | ||
|
|
87bc962c88 | ||
|
|
546ce6e229 | ||
|
|
2163d5aaf6 | ||
|
|
905ea160f2 | ||
|
|
675f4a372b | ||
|
|
7ff42db0c6 | ||
|
|
a01283a446 | ||
|
|
fefa261e7d | ||
|
|
0447b22dd2 | ||
|
|
cf125c1b48 | ||
|
|
81a9d8257c | ||
|
|
ee3f471300 | ||
|
|
5c2e5f626d | ||
|
|
a0f4b62361 | ||
|
|
786166b448 | ||
|
|
66e198b4ef | ||
|
|
4919240242 | ||
|
|
d7e6a41e3f | ||
|
|
202ef737dd | ||
|
|
04993224dc | ||
|
|
bebe7c28f8 | ||
|
|
639991dde4 | ||
|
|
31404cb89a | ||
|
|
f6205ca1dd | ||
|
|
6e86fc0593 | ||
|
|
f39a9845a3 | ||
|
|
ba17582945 | ||
|
|
02a1cbd467 | ||
|
|
2cd102ef0b | ||
|
|
240361b55b | ||
|
|
9beabc93cd | ||
|
|
8f4c6b911a | ||
|
|
083ef3010d | ||
|
|
e6c2253219 | ||
|
|
d802eb3f28 | ||
|
|
a342d5d5ad | ||
|
|
99adb77fcb | ||
|
|
2ea4eae9d6 | ||
|
|
9b079b2c3a | ||
|
|
8648e8569e | ||
|
|
1be0ab8bcb | ||
|
|
718f76c1f2 | ||
|
|
155d1f4c06 | ||
|
|
cb79e27d5a | ||
|
|
26991f8dd8 | ||
|
|
2375330d76 | ||
|
|
94e9b6d99b | ||
|
|
b516d24101 | ||
|
|
1b131d9371 | ||
|
|
3f45ef192d | ||
|
|
c6684af521 | ||
|
|
52f12b81ff | ||
|
|
6630f787bf | ||
|
|
2d7b2da3e2 | ||
|
|
d3b008fcd9 | ||
|
|
8a62fd0e6a | ||
|
|
b044860f05 | ||
|
|
1c5786dfb6 | ||
|
|
6bc9e3d695 | ||
|
|
b74fe59f12 | ||
|
|
6b57aa7f14 | ||
|
|
227125b35c | ||
|
|
c4012d8dfc | ||
|
|
cf3fa9ffbc | ||
|
|
40640d029a | ||
|
|
01eb7038dc | ||
|
|
58115bfd11 | ||
|
|
f1ea5031fb | ||
|
|
26d15a9fb3 | ||
|
|
54ba8e6047 | ||
|
|
eca063ab75 | ||
|
|
8892f4144e | ||
|
|
d2c25f9d6c | ||
|
|
b57457dc2f | ||
|
|
2861b0cfa2 | ||
|
|
0c45dbb884 | ||
|
|
a9f9261fb7 | ||
|
|
7e5f54a4f1 | ||
|
|
1228e8759c | ||
|
|
1daf771218 | ||
|
|
880cb08c3d | ||
|
|
e2cbae3089 | ||
|
|
42dcc3318c | ||
|
|
b32a85ae7e | ||
|
|
af85edddca | ||
|
|
eccd88e3c2 | ||
|
|
e0e11629a1 | ||
|
|
968095c183 | ||
|
|
d32b5115c5 | ||
|
|
d3001ec887 | ||
|
|
fef6a52008 | ||
|
|
048e6affbc | ||
|
|
c653d49691 | ||
|
|
6f5c9bd054 | ||
|
|
9e5576244d | ||
|
|
ef91317232 | ||
|
|
10c44d050f | ||
|
|
1845ea7170 | ||
|
|
d453294622 | ||
|
|
d11f9e4971 | ||
|
|
08272dd04e | ||
|
|
42441b9b42 | ||
|
|
e4a293c046 | ||
|
|
0cc5a39d63 | ||
|
|
942ea3f125 | ||
|
|
a8a70bb71c | ||
|
|
0d7c3fb4b2 | ||
|
|
77c682454e | ||
|
|
dd3473f5d8 | ||
|
|
cceadc5e04 | ||
|
|
e48c3a3f9c | ||
|
|
14981ef077 | ||
|
|
a7858d44bd | ||
|
|
9ae5f27c04 | ||
|
|
d691129842 | ||
|
|
e26d551263 | ||
|
|
277c6a444f | ||
|
|
f344800fd6 | ||
|
|
39a6fba33f | ||
|
|
8e11657bd2 | ||
|
|
dfbeaa4edf | ||
|
|
e90dc3b7f4 | ||
|
|
dba89e611a | ||
|
|
1a3fecc89e | ||
|
|
407e6a3d06 | ||
|
|
6ee19d5359 | ||
|
|
2df424dbac | ||
|
|
9874be6bf1 | ||
|
|
a3d4199d1d | ||
|
|
247fa146a9 | ||
|
|
f2b2c02cd6 | ||
|
|
a915f27f00 | ||
|
|
e14a488934 | ||
|
|
e82a8d9bc3 | ||
|
|
4527a0d12b | ||
|
|
01be202484 | ||
|
|
d1fe99edc3 | ||
|
|
fa629591e9 | ||
|
|
4ab3edc97b | ||
|
|
f1bfc6bf55 | ||
|
|
3283843ef3 | ||
|
|
4cb14ec8cc | ||
|
|
41535a68be | ||
|
|
d62447a12a | ||
|
|
802367c914 | ||
|
|
ff9b2c6ee8 | ||
|
|
a0e25c941a | ||
|
|
091c99e784 | ||
|
|
e264bb407b | ||
|
|
16625210fc | ||
|
|
2550453ee4 | ||
|
|
d1c480f23f | ||
|
|
b4b0397589 | ||
|
|
ab6b34e84c | ||
|
|
87af9d5078 | ||
|
|
95fab7c395 | ||
|
|
90825925ff | ||
|
|
7036cf9e49 | ||
|
|
53123eb0ee | ||
|
|
3c5407dd51 | ||
|
|
1ffe81f740 | ||
|
|
6bb35d61e1 | ||
|
|
f36ccf7bdc | ||
|
|
4632e68a00 | ||
|
|
09858d0783 | ||
|
|
9d1423c41b | ||
|
|
1a4b7786dd | ||
|
|
77c0a21ad0 | ||
|
|
7cedf14121 | ||
|
|
235346f3dd | ||
|
|
34c36b7c3a | ||
|
|
3e0f788ec3 | ||
|
|
867bb8a072 | ||
|
|
31a400158a | ||
|
|
8106ff6489 | ||
|
|
de3508993c | ||
|
|
fd3e7a6f8a | ||
|
|
4cf97a6054 | ||
|
|
75036e3ec7 |
15
.env.example
15
.env.example
@@ -1,13 +1,16 @@
|
|||||||
# Rename this file to `.env` and fill in the values.
|
# Rename this file to `.env` and fill in the values.
|
||||||
|
# You already have access to basic local functionality (UI, authentication, database read access).
|
||||||
|
|
||||||
# Required variables for basic local functionality
|
# Optional variables for the backend server functionality (modifying user data, etc.)
|
||||||
|
|
||||||
# For database connection. A 16-character password with digits and letters.
|
# For database write access (dev).
|
||||||
SUPABASE_DB_PASSWORD=
|
# A 16-character password with digits and letters.
|
||||||
|
SUPABASE_DB_PASSWORD=09wATRREfAzyL5pc
|
||||||
|
|
||||||
# For authentication.
|
# For Firebase access.
|
||||||
# Ask the project admin. Should start with "AIza".
|
# Open a GitHub issue with your contribution ideas and an admin will give you the key.
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY=
|
# TODO: find a way to give anyone moderate access to dev firebase.
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS_DEV="[...].json"
|
||||||
|
|
||||||
# The URL where your local backend server is running.
|
# The URL where your local backend server is running.
|
||||||
# You can change the port if needed.
|
# You can change the port if needed.
|
||||||
|
|||||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -46,10 +46,12 @@ jobs:
|
|||||||
# npx playwright install
|
# npx playwright install
|
||||||
|
|
||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_API_URL: localhost:8088
|
||||||
|
NEXT_PUBLIC_FIREBASE_ENV: PROD
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
NEXT_PUBLIC_API_URL=localhost:8088 \
|
|
||||||
NEXT_PUBLIC_FIREBASE_ENV=PROD \
|
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} \
|
|
||||||
yarn --cwd=web serve &
|
yarn --cwd=web serve &
|
||||||
npx wait-on http://localhost:3000
|
npx wait-on http://localhost:3000
|
||||||
npx playwright test tests/playwright
|
npx playwright test tests/playwright
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -55,9 +55,27 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
*prisma/migrations
|
*prisma/migrations
|
||||||
martin
|
martin
|
||||||
|
email-preview
|
||||||
.obsidian
|
.obsidian
|
||||||
.idea
|
.idea
|
||||||
*.last-run.json
|
*.last-run.json
|
||||||
|
|
||||||
*lock.hcl
|
*lock.hcl
|
||||||
/web/pages/test.tsx
|
/web/pages/test.tsx
|
||||||
|
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.gif
|
||||||
|
*.svg
|
||||||
|
*.mp4
|
||||||
|
*.mov
|
||||||
|
*.avi
|
||||||
|
*.wmv
|
||||||
|
*.mp3
|
||||||
|
*.wav
|
||||||
|
*.flac
|
||||||
|
*.aac
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ We welcome pull requests, but only if they meet the project's quality and design
|
|||||||
3. **Add the upstream remote**:
|
3. **Add the upstream remote**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git remote add upstream https://github.com/CompassMeet/Compass.git
|
git remote add upstream https://github.com/CompassConnections/Compass.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create a New Branch
|
## Create a New Branch
|
||||||
|
|||||||
126
README.md
126
README.md
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
[](https://github.com/CompassMeet/Compass/actions/workflows/ci.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
||||||
[](https://github.com/CompassMeet/Compass/actions/workflows/cd.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
|
||||||

|

|
||||||
|
|
||||||
# Compass
|
# Compass
|
||||||
|
|
||||||
This repository provides the source code for [Compass](https://compassmeet.com), a web application for people to form deep 1-on-1 relationships in a fully transparent and efficient way. And it just got released!
|
This repository contains the source code for [Compass](https://compassmeet.com) — an open platform for forming deep, authentic 1-on-1 connections with clarity and efficiency.
|
||||||
|
|
||||||
**We can’t do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/about) in any way you can and help our community thrive!
|
**We can’t do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/support) in any way you can and help our community thrive!
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -18,10 +18,15 @@ This repository provides the source code for [Compass](https://compassmeet.com),
|
|||||||
- Open source
|
- Open source
|
||||||
- Democratically governed
|
- Democratically governed
|
||||||
|
|
||||||
A detailed description of the vision is available [here](https://martinbraquet.com/meeting-rational).
|
You can find a lot of interesting info in the [About page](https://www.compassmeet.com/about) and the [FAQ](https://www.compassmeet.com/faq) as well.
|
||||||
|
A detailed description of the early vision is also available in this [blog post](https://martinbraquet.com/meeting-rational) (you can disregard the parts about rationality, as Compass shifted to a more general audience).
|
||||||
|
|
||||||
## To Do
|
## To Do
|
||||||
|
|
||||||
|
No contribution is too small—whether it’s changing a color, resizing a button, tweaking a font, or improving wording. Bigger contributions like adding new profile fields, building modules, or improving onboarding are equally welcome. The goal is to make the platform better step by step, and every improvement counts. If you see something that could be clearer, smoother, or more engaging, **please jump in**!
|
||||||
|
|
||||||
|
Here are some examples of things that would be very useful. If you want to help but don’t know where to start, just ask us on [Discord](https://discord.gg/8Vd7jzqjun).
|
||||||
|
|
||||||
- [x] Authentication (user/password and Google Sign In)
|
- [x] Authentication (user/password and Google Sign In)
|
||||||
- [x] Set up PostgreSQL in Production with supabase
|
- [x] Set up PostgreSQL in Production with supabase
|
||||||
- [x] Set up web hosting (vercel)
|
- [x] Set up web hosting (vercel)
|
||||||
@@ -29,20 +34,37 @@ A detailed description of the vision is available [here](https://martinbraquet.c
|
|||||||
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
|
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
|
||||||
- [x] Set up page listing all the profiles
|
- [x] Set up page listing all the profiles
|
||||||
- [x] Search through most profile variables
|
- [x] Search through most profile variables
|
||||||
- [x] (Set up chat / direct messaging)
|
- [x] Set up chat / direct messaging
|
||||||
- [x] Set up domain name (https://compassmeet.com)
|
- [x] Set up domain name (compassmeet.com)
|
||||||
|
- [ ] Add mobile app (React Native on Android and iOS)
|
||||||
|
- [ ] Add better onboarding (tooltips, modals, etc.)
|
||||||
|
- [ ] Add modules to learn more about each other (personality test, conflict style, love languages, etc.)
|
||||||
|
- [ ] Add modules to improve interpersonal skills (active listening, nonviolent communication, etc.)
|
||||||
|
- [ ] Add calendar integration and scheduling
|
||||||
|
- [ ] Add events (group calls, in-person meetups, etc.)
|
||||||
|
|
||||||
#### Secondary To Do
|
#### Secondary To Do
|
||||||
|
|
||||||
Any action item is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
|
Everything is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
|
||||||
|
|
||||||
|
- [x] Clean up learn more page
|
||||||
|
- [x] Add dark theme
|
||||||
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
|
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
|
||||||
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
|
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
|
||||||
- [ ] Cover with tests (very important, just the test template and framework are ready)
|
- [ ] Cover with tests (very important, just the test template and framework are ready)
|
||||||
- [ ] Clean up terms and conditions
|
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||||
- [ ] Clean up privacy notice
|
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||||
- [x] Clean up learn more page
|
- [ ] Clean up privacy notice (convert to Markdown)
|
||||||
- [x] Add dark theme
|
- [ ] Add other authentication methods (GitHub, Facebook, Apple, phone, etc.)
|
||||||
|
- [ ] Add email verification
|
||||||
|
- [ ] Add password reset
|
||||||
|
- [ ] Add automated welcome email
|
||||||
|
- [ ] Security audit and penetration testing
|
||||||
|
- [ ] Make `deploy-api.sh` run automatically on push to `main` branch
|
||||||
|
- [ ] Create settings page (change email, password, delete account, etc.)
|
||||||
|
- [ ] Improve [financials](web/public/md/financials.md) page (donor / acknowledgments, etc.)
|
||||||
|
- [ ] Improve loading sign (e.g., animation of a compass moving around)
|
||||||
|
- [ ] Show compatibility score in profile page
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -55,13 +77,13 @@ The web app is coded in Typescript using React as front-end. It includes:
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Below are all the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
|
Below are the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Clone the repo and navigating into it:
|
Fork the [repo](https://github.com/CompassConnections/Compass) on GitHub (button in top right). Then, clone your repo and navigating into it:
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:CompassMeet/Compass.git
|
git clone https://github.com/<your-username>/Compass.git
|
||||||
cd Compass
|
cd Compass
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -69,7 +91,7 @@ Install `opentofu`, `docker`, and `yarn`. Try running this on Linux or macOS for
|
|||||||
```bash
|
```bash
|
||||||
./setup.sh
|
./setup.sh
|
||||||
```
|
```
|
||||||
If it doesn't work, you can install them manually (Google how to install `opentofu`, `docker`, and `yarn` for your OS).
|
If it doesn't work, you can install them manually (google how to install `opentofu`, `docker`, and `yarn` for your OS).
|
||||||
|
|
||||||
Then, install the dependencies for this project:
|
Then, install the dependencies for this project:
|
||||||
```bash
|
```bash
|
||||||
@@ -83,54 +105,23 @@ We can't make the following information public, for security and privacy reasons
|
|||||||
- Firebase, otherwise anyone could remove users or modify the media files
|
- Firebase, otherwise anyone could remove users or modify the media files
|
||||||
- Email, analytics, and location services, otherwise anyone could use our paid plan
|
- Email, analytics, and location services, otherwise anyone could use our paid plan
|
||||||
|
|
||||||
So, for your development, we will give you user-specific access when possible (e.g., Firebase) and for the rest you will need to set up cloned services (email, locations, etc.) and store your secrets as environment variables.
|
We separate all those services between production and local development, so that you can code freely without impacting the functioning of the platform.
|
||||||
|
Contributors should use the default keys for local development. Production uses a separate environment with stricter rules and private keys that are not shared.
|
||||||
|
|
||||||
To do so, simply create an `.env` file as a copy of `.env.example`, open it, and fill in the variables according to the instructions in the file:
|
Most of the code will work out of the box. All you need to do is creating an `.env` file as a copy of `.env.example`:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Installing PostgreSQL
|
If you do need one of the few remaining services, you need to store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file.
|
||||||
|
|
||||||
Run the following commands to set up your local development database. Run only the section that corresponds to your operating system.
|
|
||||||
|
|
||||||
On macOS:
|
|
||||||
```bash
|
|
||||||
brew install postgresql
|
|
||||||
brew services start postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
On Linux:
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install postgresql postgresql-contrib
|
|
||||||
sudo systemctl start postgresql
|
|
||||||
````
|
|
||||||
|
|
||||||
On Windows, you can download PostgreSQL from the [official website](https://www.postgresql.org/download/windows/).
|
|
||||||
|
|
||||||
### Database Initialization
|
|
||||||
|
|
||||||
Create a database named `compass` and set the password for the `postgres` user:
|
|
||||||
```bash
|
|
||||||
sudo -u postgres psql
|
|
||||||
ALTER USER postgres WITH PASSWORD 'password';
|
|
||||||
\q
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the database
|
|
||||||
```bash
|
|
||||||
...
|
|
||||||
```
|
|
||||||
Note that your local database will be made of synthetic data, not real users. This is fine for development and testing.
|
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
Make sure the tests pass:
|
Make sure the tests pass:
|
||||||
```bash
|
```bash
|
||||||
yarn test
|
yarn test tests/jest/
|
||||||
```
|
```
|
||||||
TODO: fix tests
|
TODO: make `yarn test` run all the tests, not just the ones in `tests/jest/`.
|
||||||
|
|
||||||
### Running the Development Server
|
### Running the Development Server
|
||||||
|
|
||||||
@@ -139,11 +130,38 @@ Start the development server:
|
|||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see 5 synthetic profiles.
|
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see a few synthetic profiles.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
Now you can start contributing by making changes and submitting pull requests!
|
Now you can start contributing by making changes and submitting pull requests!
|
||||||
|
|
||||||
See [development.md](docs/development.md) for additional instructions, such as adding new profile features.
|
See [development.md](docs/development.md) for additional instructions, such as adding new profile features.
|
||||||
|
|
||||||
|
### Submission
|
||||||
|
|
||||||
|
Add the original repo as upstream for syncing:
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/CompassConnections/Compass.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new branch for your changes:
|
||||||
|
```bash
|
||||||
|
git checkout -b <branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Make changes, then stage and commit:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Describe your changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Push branch to your fork:
|
||||||
|
```bash
|
||||||
|
git push origin <branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, open a Pull Request on GitHub from your `fork/<branch-name>` → `CompassConnections/Compass` main branch.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections.
|
This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections.
|
||||||
@@ -8,5 +8,5 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Contact the development team to report a vulnerability. You should receive updates within a week.
|
Contact the development team at compass.meet.info@gmail.com to report a vulnerability. You should receive updates within a week.
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
# prereq: first do `yarn build` to compile typescript & etc.
|
# prereq: first do `yarn build` to compile typescript & etc.
|
||||||
|
|
||||||
FROM node:19-alpine
|
FROM node:20-alpine
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
# Install PM2 globally
|
# Install PM2 globally
|
||||||
RUN yarn global add pm2
|
RUN yarn global add pm2
|
||||||
|
|
||||||
# Remove?
|
# Fet dependencies in for efficient docker layering
|
||||||
COPY tsconfig.json ./
|
|
||||||
|
|
||||||
# first get dependencies in for efficient docker layering
|
|
||||||
COPY dist/package.json dist/yarn.lock ./
|
COPY dist/package.json dist/yarn.lock ./
|
||||||
RUN yarn install --frozen-lockfile --production
|
|
||||||
|
|
||||||
# then copy over typescript payload
|
# Clean yarn cache to reduce image size
|
||||||
|
RUN yarn install --frozen-lockfile --production && \
|
||||||
|
yarn cache clean --force && \
|
||||||
|
rm -rf /usr/local/share/.cache/yarn
|
||||||
|
|
||||||
|
# Copy over typescript payload
|
||||||
COPY dist ./
|
COPY dist ./
|
||||||
|
|
||||||
# Copy the PM2 ecosystem configuration
|
# Copy the PM2 ecosystem configuration
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
# Backend API
|
# Backend API
|
||||||
|
|
||||||
This is the code for the API running at `api.compassmeet.com`.
|
This is the code for the API running at https://api.compassmeet.com.
|
||||||
It runs in a docker inside a Google Cloud virtual machine.
|
It runs in a docker inside a Google Cloud virtual machine.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
You must have the `gcloud` CLI.
|
You must have the `gcloud` CLI.
|
||||||
|
|
||||||
On MacOS:
|
On macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install --cask google-cloud-sdk
|
brew install --cask google-cloud-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
On Linux:
|
On Linux:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get update && sudo apt-get install google-cloud-sdk
|
sudo apt-get update && sudo apt-get install google-cloud-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gcloud init
|
gcloud init
|
||||||
gcloud auth login
|
gcloud auth login
|
||||||
gcloud config set project YOUR_PROJECT_ID
|
gcloud config set project YOUR_PROJECT_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
This section is only for the people who are creating a server from scratch, for instance for a forked project.
|
This section is only for the people who are creating a server from scratch, for instance for a forked project.
|
||||||
|
|
||||||
One-time commands you may need to run:
|
One-time commands you may need to run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gcloud artifacts repositories create builds \
|
gcloud artifacts repositories create builds \
|
||||||
--repository-format=docker \
|
--repository-format=docker \
|
||||||
@@ -51,6 +56,20 @@ gcloud projects add-iam-policy-binding compass-130ba \
|
|||||||
gcloud run services list
|
gcloud run services list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Set up the saved search notifications job:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcloud scheduler jobs create http daily-saved-search-notifications \
|
||||||
|
--schedule="0 16 * * *" \
|
||||||
|
--uri="https://api.compassmeet.com/internal/send-search-notifications" \
|
||||||
|
--http-method=POST \
|
||||||
|
--headers="x-api-key=<API_KEY>" \
|
||||||
|
--time-zone="UTC" \
|
||||||
|
--location=us-west1
|
||||||
|
```
|
||||||
|
|
||||||
|
View it [here](https://console.cloud.google.com/cloudscheduler).
|
||||||
|
|
||||||
##### DNS
|
##### DNS
|
||||||
|
|
||||||
* After deployment, Terraform assigns a static external IP to this resource.
|
* After deployment, Terraform assigns a static external IP to this resource.
|
||||||
@@ -60,6 +79,7 @@ gcloud run services list
|
|||||||
gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)"
|
gcloud compute addresses describe api-lb-ip-2 --global --format="get(address)"
|
||||||
34.117.20.215
|
34.117.20.215
|
||||||
```
|
```
|
||||||
|
|
||||||
Since Vercel manages your domain (`compassmeet.com`):
|
Since Vercel manages your domain (`compassmeet.com`):
|
||||||
|
|
||||||
1. Log in to [Vercel dashboard](https://vercel.com/dashboard).
|
1. Log in to [Vercel dashboard](https://vercel.com/dashboard).
|
||||||
@@ -67,7 +87,7 @@ Since Vercel manages your domain (`compassmeet.com`):
|
|||||||
3. Add an **A record** for your API subdomain:
|
3. Add an **A record** for your API subdomain:
|
||||||
|
|
||||||
| Type | Name | Value | TTL |
|
| Type | Name | Value | TTL |
|
||||||
| ---- | ---- | ------------ | ----- |
|
|------|------|--------------|-------|
|
||||||
| A | api | 34.123.45.67 | 600 s |
|
| A | api | 34.123.45.67 | 600 s |
|
||||||
|
|
||||||
* `Name` is just the subdomain: `api` → `api.compassmeet.com`.
|
* `Name` is just the subdomain: `api` → `api.compassmeet.com`.
|
||||||
@@ -85,7 +105,6 @@ curl -I https://api.compassmeet.com
|
|||||||
* `nslookup` should return the LB IP (`34.123.45.67`).
|
* `nslookup` should return the LB IP (`34.123.45.67`).
|
||||||
* `curl -I` should return `200 OK` from your service.
|
* `curl -I` should return `200 OK` from your service.
|
||||||
|
|
||||||
|
|
||||||
If SSL isn’t ready (may take 15 mins), check LB logs:
|
If SSL isn’t ready (may take 15 mins), check LB logs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -96,7 +115,9 @@ gcloud compute ssl-certificates describe api-lb-cert-2
|
|||||||
|
|
||||||
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords).
|
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords).
|
||||||
|
|
||||||
Add the secrets for your specific project in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine can access them.
|
Add the secrets for your specific project
|
||||||
|
in [Google Cloud Secrets manager](https://console.cloud.google.com/security/secret-manager), so that the virtual machine
|
||||||
|
can access them.
|
||||||
|
|
||||||
For Compass, the name of the secrets are in [secrets.ts](../../common/src/secrets.ts).
|
For Compass, the name of the secrets are in [secrets.ts](../../common/src/secrets.ts).
|
||||||
|
|
||||||
@@ -111,13 +132,16 @@ In root directory, run the local api with hot reload, along with all the other b
|
|||||||
### Deploy
|
### Deploy
|
||||||
|
|
||||||
Run in this directory to deploy your code to the server.
|
Run in this directory to deploy your code to the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy-api.sh prod
|
./deploy-api.sh prod
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connect to the server
|
### Connect to the server
|
||||||
|
|
||||||
Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs, files, debug, etc.
|
Run in this directory to connect to the API server running as virtual machine in Google Cloud. You can access logs,
|
||||||
|
files, debug, etc.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./ssh-api.sh prod
|
./ssh-api.sh prod
|
||||||
```
|
```
|
||||||
@@ -128,4 +152,11 @@ Useful commands once inside the server:
|
|||||||
sudo journalctl -u konlet-startup --no-pager -efb
|
sudo journalctl -u konlet-startup --no-pager -efb
|
||||||
sudo docker logs -f $(sudo docker ps -alq)
|
sudo docker logs -f $(sudo docker ps -alq)
|
||||||
docker exec -it $(sudo docker ps -alq) sh
|
docker exec -it $(sudo docker ps -alq) sh
|
||||||
|
docker run -it --rm $(docker images -q | head -n 1) sh
|
||||||
|
docker rmi -f $(docker images -aq)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
The API docs are available at https://api.compassmeet.com. They are defined in [openapi.json](openapi.json).
|
||||||
|
Just a few endpoints are mentioned in that JSON doc. Feel free to help by adding the remaining ones!
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
source ../../.env
|
source ../../.env
|
||||||
|
|
||||||
ENV=${1:-prod}
|
ENV=${1:-prod}
|
||||||
@@ -28,7 +30,6 @@ IMAGE_TAG="${TIMESTAMP}-${GIT_REVISION}"
|
|||||||
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
|
IMAGE_URL="${REGION}-docker.pkg.dev/${PROJECT}/builds/${SERVICE_NAME}:${IMAGE_TAG}"
|
||||||
|
|
||||||
echo "🚀 Deploying ${SERVICE_NAME} to ${ENV} ($(date "+%Y-%m-%d %I:%M:%S %p"))"
|
echo "🚀 Deploying ${SERVICE_NAME} to ${ENV} ($(date "+%Y-%m-%d %I:%M:%S %p"))"
|
||||||
yarn add tsconfig-paths
|
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin us-west1-docker.pkg.dev
|
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin us-west1-docker.pkg.dev
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name: "api",
|
name: "api",
|
||||||
script: "node",
|
script: "node",
|
||||||
args: "-r tsconfig-paths/register --dns-result-order=ipv4first backend/api/lib/serve.js",
|
args: "--dns-result-order=ipv4first backend/api/lib/serve.js",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
NODE_PATH: "/usr/src/app/node_modules", // <- ensures Node finds tsconfig-paths
|
NODE_PATH: "/usr/src/app/node_modules", // <- ensures Node finds tsconfig-paths
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ resource "google_compute_backend_service" "api_backend" {
|
|||||||
# URL map
|
# URL map
|
||||||
resource "google_compute_url_map" "api_url_map" {
|
resource "google_compute_url_map" "api_url_map" {
|
||||||
name = "${local.service_name}-url-map"
|
name = "${local.service_name}-url-map"
|
||||||
default_service = google_compute_backend_service.api_backend.id
|
default_service = google_compute_backend_service.api_backend.self_link
|
||||||
|
|
||||||
host_rule {
|
host_rule {
|
||||||
hosts = ["*"]
|
hosts = ["*"]
|
||||||
@@ -185,9 +185,33 @@ resource "google_compute_url_map" "api_url_map" {
|
|||||||
path_matcher {
|
path_matcher {
|
||||||
name = "allpaths"
|
name = "allpaths"
|
||||||
default_service = google_compute_backend_service.api_backend.self_link
|
default_service = google_compute_backend_service.api_backend.self_link
|
||||||
|
|
||||||
|
# Priority 0: passthrough /v0/* requests
|
||||||
|
route_rules {
|
||||||
|
priority = 1
|
||||||
|
match_rules {
|
||||||
|
prefix_match = "/v0"
|
||||||
|
}
|
||||||
|
service = google_compute_backend_service.api_backend.self_link
|
||||||
|
}
|
||||||
|
|
||||||
|
# Priority 1: rewrite everything else to /v0
|
||||||
|
route_rules {
|
||||||
|
priority = 2
|
||||||
|
match_rules {
|
||||||
|
prefix_match = "/"
|
||||||
|
}
|
||||||
|
route_action {
|
||||||
|
url_rewrite {
|
||||||
|
path_prefix_rewrite = "/v0/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service = google_compute_backend_service.api_backend.self_link
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# HTTPS proxy
|
# HTTPS proxy
|
||||||
resource "google_compute_target_https_proxy" "api_https_proxy" {
|
resource "google_compute_target_https_proxy" "api_https_proxy" {
|
||||||
name = "${local.service_name}-https-proxy"
|
name = "${local.service_name}-https-proxy"
|
||||||
|
|||||||
29
backend/api/openapi.json
Normal file
29
backend/api/openapi.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Compass API",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Health",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/get-profiles": {
|
||||||
|
"get": {
|
||||||
|
"summary": "List profiles",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@compass/api",
|
"name": "@compass/api",
|
||||||
"description": "Backend API endpoints",
|
"description": "Backend API endpoints",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
|
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"",
|
"debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"",
|
||||||
"dist": "yarn dist:clean && yarn dist:copy",
|
"dist": "yarn dist:clean && yarn dist:copy",
|
||||||
"dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib",
|
"dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib",
|
||||||
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist",
|
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist && cp openapi.json dist",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"verify": "yarn --cwd=../.. verify",
|
"verify": "yarn --cwd=../.. verify",
|
||||||
"verify:dir": "npx eslint . --max-warnings 0",
|
"verify:dir": "npx eslint . --max-warnings 0",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"regen-types-dev": "cd ../supabase && make ENV=dev regen-types"
|
"regen-types-dev": "cd ../supabase && make ENV=dev regen-types"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"main": "src/serve.ts",
|
"main": "src/serve.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -45,24 +45,28 @@
|
|||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dayjs": "1.11.4",
|
"dayjs": "1.11.4",
|
||||||
"express": "4.18.1",
|
"express": "4.18.1",
|
||||||
"firebase-admin": "11.11.1",
|
"firebase-admin": "13.5.0",
|
||||||
"gcp-metadata": "6.1.0",
|
"gcp-metadata": "6.1.0",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "9.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"pg-promise": "11.4.1",
|
"pg-promise": "11.4.1",
|
||||||
"posthog-node": "4.11.0",
|
"posthog-node": "4.11.0",
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"react-email": "3.0.7",
|
|
||||||
"resend": "4.1.2",
|
"resend": "4.1.2",
|
||||||
"string-similarity": "4.0.4",
|
"string-similarity": "4.0.4",
|
||||||
|
"swagger-jsdoc": "6.2.8",
|
||||||
|
"swagger-ui-express": "5.0.1",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twitter-api-v2": "1.15.0",
|
"twitter-api-v2": "1.15.0",
|
||||||
"ws": "8.17.0",
|
"ws": "8.17.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
"zod": "3.21.4"
|
"zod": "3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "2.8.17",
|
"@types/cors": "2.8.17",
|
||||||
|
"@types/react": "18.3.5",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
"@types/ws": "8.5.10"
|
"@types/ws": "8.5.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
import { API, type APIPath } from 'common/api/schema'
|
import {API, type APIPath} from 'common/api/schema'
|
||||||
import { APIError, pathWithPrefix } from 'common/api/utils'
|
import {APIError, pathWithPrefix} from 'common/api/utils'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import express from 'express'
|
import express, {type ErrorRequestHandler, type RequestHandler} from 'express'
|
||||||
import { type ErrorRequestHandler, type RequestHandler } from 'express'
|
import {hrtime} from 'node:process'
|
||||||
import { hrtime } from 'node:process'
|
import {withMonitoringContext} from 'shared/monitoring/context'
|
||||||
import { withMonitoringContext } from 'shared/monitoring/context'
|
import {log} from 'shared/monitoring/log'
|
||||||
import { log } from 'shared/monitoring/log'
|
import {metrics} from 'shared/monitoring/metrics'
|
||||||
import { metrics } from 'shared/monitoring/metrics'
|
import {banUser} from './ban-user'
|
||||||
import { banUser } from './ban-user'
|
import {blockUser, unblockUser} from './block-user'
|
||||||
import { blockUser, unblockUser } from './block-user'
|
import {getCompatibleProfilesHandler} from './compatible-profiles'
|
||||||
import { getCompatibleLoversHandler } from './compatible-lovers'
|
import {createComment} from './create-comment'
|
||||||
import { createComment } from './create-comment'
|
import {createCompatibilityQuestion} from './create-compatibility-question'
|
||||||
import { createCompatibilityQuestion } from './create-compatibility-question'
|
import {createProfile} from './create-profile'
|
||||||
import { createLover } from './create-lover'
|
import {createUser} from './create-user'
|
||||||
import { createUser } from './create-user'
|
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
||||||
import { getCompatibilityQuestions } from './get-compatibililty-questions'
|
import {getLikesAndShips} from './get-likes-and-ships'
|
||||||
import { getLikesAndShips } from './get-likes-and-ships'
|
import {getProfileAnswers} from './get-profile-answers'
|
||||||
import { getLoverAnswers } from './get-lover-answers'
|
import {getProfiles} from './get-profiles'
|
||||||
import { getLovers } from './get-lovers'
|
import {getSupabaseToken} from './get-supabase-token'
|
||||||
import { getSupabaseToken } from './get-supabase-token'
|
import {getDisplayUser, getUser} from './get-user'
|
||||||
import { getDisplayUser, getUser } from './get-user'
|
import {getMe} from './get-me'
|
||||||
import { getMe } from './get-me'
|
import {hasFreeLike} from './has-free-like'
|
||||||
import { hasFreeLike } from './has-free-like'
|
import {health} from './health'
|
||||||
import { health } from './health'
|
import {type APIHandler, typedEndpoint} from './helpers/endpoint'
|
||||||
import { typedEndpoint, type APIHandler } from './helpers/endpoint'
|
import {hideComment} from './hide-comment'
|
||||||
import { hideComment } from './hide-comment'
|
import {likeProfile} from './like-profile'
|
||||||
import { likeLover } from './like-lover'
|
import {markAllNotifsRead} from './mark-all-notifications-read'
|
||||||
import { markAllNotifsRead } from './mark-all-notifications-read'
|
import {removePinnedPhoto} from './remove-pinned-photo'
|
||||||
import { removePinnedPhoto } from './remove-pinned-photo'
|
import {report} from './report'
|
||||||
import { report } from './report'
|
import {searchLocation} from './search-location'
|
||||||
import { searchLocation } from './search-location'
|
import {searchNearCity} from './search-near-city'
|
||||||
import { searchNearCity } from './search-near-city'
|
import {shipProfiles} from './ship-profiles'
|
||||||
import { shipLovers } from './ship-lovers'
|
import {starProfile} from './star-profile'
|
||||||
import { starLover } from './star-lover'
|
import {updateProfile} from './update-profile'
|
||||||
import { updateLover } from './update-lover'
|
import {updateMe} from './update-me'
|
||||||
import { updateMe } from './update-me'
|
import {deleteMe} from './delete-me'
|
||||||
import { deleteMe } from './delete-me'
|
import {getCurrentPrivateUser} from './get-current-private-user'
|
||||||
import { getCurrentPrivateUser } from './get-current-private-user'
|
import {createPrivateUserMessage} from './create-private-user-message'
|
||||||
import { createPrivateUserMessage } from './create-private-user-message'
|
|
||||||
import {
|
import {
|
||||||
getChannelMemberships,
|
getChannelMemberships,
|
||||||
getChannelMessages,
|
getChannelMessages,
|
||||||
getLastSeenChannelTime,
|
getLastSeenChannelTime,
|
||||||
setChannelLastSeenTime,
|
setChannelLastSeenTime,
|
||||||
} from 'api/get-private-messages'
|
} from 'api/get-private-messages'
|
||||||
import { searchUsers } from './search-users'
|
import {searchUsers} from './search-users'
|
||||||
import { createPrivateUserMessageChannel } from './create-private-user-message-channel'
|
import {createPrivateUserMessageChannel} from './create-private-user-message-channel'
|
||||||
import { leavePrivateUserMessageChannel } from './leave-private-user-message-channel'
|
import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel'
|
||||||
import { updatePrivateUserMessageChannel } from './update-private-user-message-channel'
|
import {updatePrivateUserMessageChannel} from './update-private-user-message-channel'
|
||||||
import { getNotifications } from './get-notifications'
|
import {getNotifications} from './get-notifications'
|
||||||
import { updateNotifSettings } from './update-notif-setting'
|
import {updateNotifSettings} from './update-notif-setting'
|
||||||
|
import swaggerUi from "swagger-ui-express"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import {sendSearchNotifications} from "api/send-search-notifications";
|
||||||
|
|
||||||
const allowCorsUnrestricted: RequestHandler = cors({})
|
const allowCorsUnrestricted: RequestHandler = cors({})
|
||||||
|
|
||||||
@@ -66,15 +68,15 @@ const requestMonitoring: RequestHandler = (req, _res, next) => {
|
|||||||
const traceId = traceContext
|
const traceId = traceContext
|
||||||
? traceContext.split('/')[0]
|
? traceContext.split('/')[0]
|
||||||
: crypto.randomUUID()
|
: crypto.randomUUID()
|
||||||
const context = { endpoint: req.path, traceId }
|
const context = {endpoint: req.path, traceId}
|
||||||
withMonitoringContext(context, () => {
|
withMonitoringContext(context, () => {
|
||||||
const startTs = hrtime.bigint()
|
const startTs = hrtime.bigint()
|
||||||
log(`${req.method} ${req.url}`)
|
log(`${req.method} ${req.url}`)
|
||||||
metrics.inc('http/request_count', { endpoint: req.path })
|
metrics.inc('http/request_count', {endpoint: req.path})
|
||||||
next()
|
next()
|
||||||
const endTs = hrtime.bigint()
|
const endTs = hrtime.bigint()
|
||||||
const latencyMs = Number(endTs - startTs) / 1e6
|
const latencyMs = Number(endTs - startTs) / 1e6
|
||||||
metrics.push('http/request_latency', latencyMs, { endpoint: req.path })
|
metrics.push('http/request_latency', latencyMs, {endpoint: req.path})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
if (error instanceof APIError) {
|
if (error instanceof APIError) {
|
||||||
log.info(error)
|
log.info(error)
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
const output: { [k: string]: unknown } = { message: error.message }
|
const output: { [k: string]: unknown } = {message: error.message}
|
||||||
if (error.details != null) {
|
if (error.details != null) {
|
||||||
output.details = error.details
|
output.details = error.details
|
||||||
}
|
}
|
||||||
@@ -91,7 +93,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
} else {
|
} else {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({ message: error.stack, error })
|
res.status(500).json({message: error.stack, error})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,22 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
export const app = express()
|
export const app = express()
|
||||||
app.use(requestMonitoring)
|
app.use(requestMonitoring)
|
||||||
|
|
||||||
|
const swaggerDocument = JSON.parse(fs.readFileSync("./openapi.json", "utf-8"))
|
||||||
|
swaggerDocument.info = {
|
||||||
|
...swaggerDocument.info,
|
||||||
|
description: "Compass is a free, open-source platform to help people form deep, meaningful, and lasting connections — whether platonic, romantic, or collaborative. It’s made possible by contributions from the community, including code, ideas, feedback, and donations. Unlike typical apps, Compass prioritizes values, interests, and personality over swipes and ads, giving you full control over who you discover and how you connect.",
|
||||||
|
version: "1.0.0",
|
||||||
|
contact: {
|
||||||
|
name: "Compass",
|
||||||
|
email: "compass.meet.info@gmail.com",
|
||||||
|
url: "https://compassmeet.com"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootPath = pathWithPrefix("/")
|
||||||
|
app.get(rootPath, swaggerUi.setup(swaggerDocument))
|
||||||
|
app.use(rootPath, swaggerUi.serve)
|
||||||
|
|
||||||
app.options('*', allowCorsUnrestricted)
|
app.options('*', allowCorsUnrestricted)
|
||||||
|
|
||||||
const handlers: { [k in APIPath]: APIHandler<k> } = {
|
const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||||
@@ -116,26 +134,26 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
|||||||
'ban-user': banUser,
|
'ban-user': banUser,
|
||||||
report: report,
|
report: report,
|
||||||
'create-user': createUser,
|
'create-user': createUser,
|
||||||
'create-lover': createLover,
|
'create-profile': createProfile,
|
||||||
me: getMe,
|
me: getMe,
|
||||||
'me/private': getCurrentPrivateUser,
|
'me/private': getCurrentPrivateUser,
|
||||||
'me/update': updateMe,
|
'me/update': updateMe,
|
||||||
'update-notif-settings': updateNotifSettings,
|
'update-notif-settings': updateNotifSettings,
|
||||||
'me/delete': deleteMe,
|
'me/delete': deleteMe,
|
||||||
'update-lover': updateLover,
|
'update-profile': updateProfile,
|
||||||
'like-lover': likeLover,
|
'like-profile': likeProfile,
|
||||||
'ship-lovers': shipLovers,
|
'ship-profiles': shipProfiles,
|
||||||
'get-likes-and-ships': getLikesAndShips,
|
'get-likes-and-ships': getLikesAndShips,
|
||||||
'has-free-like': hasFreeLike,
|
'has-free-like': hasFreeLike,
|
||||||
'star-lover': starLover,
|
'star-profile': starProfile,
|
||||||
'get-lovers': getLovers,
|
'get-profiles': getProfiles,
|
||||||
'get-lover-answers': getLoverAnswers,
|
'get-profile-answers': getProfileAnswers,
|
||||||
'get-compatibility-questions': getCompatibilityQuestions,
|
'get-compatibility-questions': getCompatibilityQuestions,
|
||||||
'remove-pinned-photo': removePinnedPhoto,
|
'remove-pinned-photo': removePinnedPhoto,
|
||||||
'create-comment': createComment,
|
'create-comment': createComment,
|
||||||
'hide-comment': hideComment,
|
'hide-comment': hideComment,
|
||||||
'create-compatibility-question': createCompatibilityQuestion,
|
'create-compatibility-question': createCompatibilityQuestion,
|
||||||
'compatible-lovers': getCompatibleLoversHandler,
|
'compatible-profiles': getCompatibleProfilesHandler,
|
||||||
'search-location': searchLocation,
|
'search-location': searchLocation,
|
||||||
'search-near-city': searchNearCity,
|
'search-near-city': searchNearCity,
|
||||||
'create-private-user-message': createPrivateUserMessage,
|
'create-private-user-message': createPrivateUserMessage,
|
||||||
@@ -151,7 +169,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
|||||||
Object.entries(handlers).forEach(([path, handler]) => {
|
Object.entries(handlers).forEach(([path, handler]) => {
|
||||||
const api = API[path as APIPath]
|
const api = API[path as APIPath]
|
||||||
const cache = cacheController((api as any).cache)
|
const cache = cacheController((api as any).cache)
|
||||||
const url = '/' + pathWithPrefix(path as APIPath)
|
const url = pathWithPrefix('/' + path as APIPath)
|
||||||
|
|
||||||
const apiRoute = [
|
const apiRoute = [
|
||||||
url,
|
url,
|
||||||
@@ -173,6 +191,27 @@ Object.entries(handlers).forEach(([path, handler]) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// console.log('COMPASS_API_KEY:', process.env.COMPASS_API_KEY)
|
||||||
|
|
||||||
|
// Internal Endpoints
|
||||||
|
app.post(pathWithPrefix("/internal/send-search-notifications"),
|
||||||
|
async (req, res) => {
|
||||||
|
const apiKey = req.header("x-api-key");
|
||||||
|
if (apiKey !== process.env.COMPASS_API_KEY) {
|
||||||
|
return res.status(401).json({error: "Unauthorized"});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sendSearchNotifications()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to send notifications:", err);
|
||||||
|
return res.status(500).json({error: "Internal server error"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
app.use(allowCorsUnrestricted, (req, res) => {
|
app.use(allowCorsUnrestricted, (req, res) => {
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
@@ -181,7 +220,7 @@ app.use(allowCorsUnrestricted, (req, res) => {
|
|||||||
.status(404)
|
.status(404)
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.json({
|
.json({
|
||||||
message: `The requested route '${req.path}' does not exist. Please check your URL for any misspellings or refer to app.ts`,
|
message: `This is the Compass API, but the requested route '${req.path}' does not exist. Please check your URL for any misspellings, the docs at https://api.compassmeet.com, or simply refer to app.ts on GitHub`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { groupBy, sortBy } from 'lodash'
|
|
||||||
import { APIError, type APIHandler } from 'api/helpers/endpoint'
|
|
||||||
import { getCompatibilityScore } from 'common/love/compatibility-score'
|
|
||||||
import {
|
|
||||||
getLover,
|
|
||||||
getCompatibilityAnswers,
|
|
||||||
getGenderCompatibleLovers,
|
|
||||||
} from 'shared/love/supabase'
|
|
||||||
import { log } from 'shared/utils'
|
|
||||||
|
|
||||||
export const getCompatibleLoversHandler: APIHandler<
|
|
||||||
'compatible-lovers'
|
|
||||||
> = async (props) => {
|
|
||||||
return getCompatibleLovers(props.userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCompatibleLovers = async (userId: string) => {
|
|
||||||
const lover = await getLover(userId)
|
|
||||||
|
|
||||||
log('got lover', {
|
|
||||||
id: lover?.id,
|
|
||||||
userId: lover?.user_id,
|
|
||||||
username: lover?.user?.username,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!lover) throw new APIError(404, 'Lover not found')
|
|
||||||
|
|
||||||
const lovers = await getGenderCompatibleLovers(lover)
|
|
||||||
|
|
||||||
const loverAnswers = await getCompatibilityAnswers([
|
|
||||||
userId,
|
|
||||||
...lovers.map((l) => l.user_id),
|
|
||||||
])
|
|
||||||
log('got lover answers ' + loverAnswers.length)
|
|
||||||
|
|
||||||
const answersByUserId = groupBy(loverAnswers, 'creator_id')
|
|
||||||
const loverCompatibilityScores = Object.fromEntries(
|
|
||||||
lovers.map(
|
|
||||||
(l) =>
|
|
||||||
[
|
|
||||||
l.user_id,
|
|
||||||
getCompatibilityScore(
|
|
||||||
answersByUserId[lover.user_id] ?? [],
|
|
||||||
answersByUserId[l.user_id] ?? []
|
|
||||||
),
|
|
||||||
] as const
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const sortedCompatibleLovers = sortBy(
|
|
||||||
lovers,
|
|
||||||
(l) => loverCompatibilityScores[l.user_id].score
|
|
||||||
).reverse()
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
lover,
|
|
||||||
compatibleLovers: sortedCompatibleLovers,
|
|
||||||
loverCompatibilityScores,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
61
backend/api/src/compatible-profiles.ts
Normal file
61
backend/api/src/compatible-profiles.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { groupBy, sortBy } from 'lodash'
|
||||||
|
import { APIError, type APIHandler } from 'api/helpers/endpoint'
|
||||||
|
import { getCompatibilityScore } from 'common/love/compatibility-score'
|
||||||
|
import {
|
||||||
|
getProfile,
|
||||||
|
getCompatibilityAnswers,
|
||||||
|
getGenderCompatibleProfiles,
|
||||||
|
} from 'shared/love/supabase'
|
||||||
|
import { log } from 'shared/utils'
|
||||||
|
|
||||||
|
export const getCompatibleProfilesHandler: APIHandler<
|
||||||
|
'compatible-profiles'
|
||||||
|
> = async (props) => {
|
||||||
|
return getCompatibleProfiles(props.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCompatibleProfiles = async (userId: string) => {
|
||||||
|
const profile = await getProfile(userId)
|
||||||
|
|
||||||
|
log('got profile', {
|
||||||
|
id: profile?.id,
|
||||||
|
userId: profile?.user_id,
|
||||||
|
username: profile?.user?.username,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!profile) throw new APIError(404, 'Profile not found')
|
||||||
|
|
||||||
|
const profiles = await getGenderCompatibleProfiles(profile)
|
||||||
|
|
||||||
|
const profileAnswers = await getCompatibilityAnswers([
|
||||||
|
userId,
|
||||||
|
...profiles.map((l) => l.user_id),
|
||||||
|
])
|
||||||
|
log('got profile answers ' + profileAnswers.length)
|
||||||
|
|
||||||
|
const answersByUserId = groupBy(profileAnswers, 'creator_id')
|
||||||
|
const profileCompatibilityScores = Object.fromEntries(
|
||||||
|
profiles.map(
|
||||||
|
(l) =>
|
||||||
|
[
|
||||||
|
l.user_id,
|
||||||
|
getCompatibilityScore(
|
||||||
|
answersByUserId[profile.user_id] ?? [],
|
||||||
|
answersByUserId[l.user_id] ?? []
|
||||||
|
),
|
||||||
|
] as const
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const sortedCompatibleProfiles = sortBy(
|
||||||
|
profiles,
|
||||||
|
(l) => profileCompatibilityScores[l.user_id].score
|
||||||
|
).reverse()
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
profile,
|
||||||
|
compatibleProfiles: sortedCompatibleProfiles,
|
||||||
|
profileCompatibilityScores,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,8 +32,8 @@ export const createComment: APIHandler<'create-comment'> = async (
|
|||||||
if (!onUser) throw new APIError(404, 'User not found')
|
if (!onUser) throw new APIError(404, 'User not found')
|
||||||
|
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const comment = await pg.one<Row<'lover_comments'>>(
|
const comment = await pg.one<Row<'profile_comments'>>(
|
||||||
`insert into lover_comments (user_id, user_name, user_username, user_avatar_url, on_user_id, content, reply_to_comment_id)
|
`insert into profile_comments (user_id, user_name, user_username, user_avatar_url, on_user_id, content, reply_to_comment_id)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7) returning *`,
|
values ($1, $2, $3, $4, $5, $6, $7) returning *`,
|
||||||
[
|
[
|
||||||
creator.id,
|
creator.id,
|
||||||
@@ -46,7 +46,7 @@ export const createComment: APIHandler<'create-comment'> = async (
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if (onUser.id !== creator.id)
|
if (onUser.id !== creator.id)
|
||||||
await createNewCommentOnLoverNotification(
|
await createNewCommentOnProfileNotification(
|
||||||
onUser,
|
onUser,
|
||||||
creator,
|
creator,
|
||||||
richTextToString(content),
|
richTextToString(content),
|
||||||
@@ -84,7 +84,7 @@ const validateComment = async (
|
|||||||
return { content, creator }
|
return { content, creator }
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewCommentOnLoverNotification = async (
|
const createNewCommentOnProfileNotification = async (
|
||||||
onUser: User,
|
onUser: User,
|
||||||
creator: User,
|
creator: User,
|
||||||
sourceText: string,
|
sourceText: string,
|
||||||
@@ -104,7 +104,7 @@ const createNewCommentOnLoverNotification = async (
|
|||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
isSeen: false,
|
isSeen: false,
|
||||||
sourceId: commentId.toString(),
|
sourceId: commentId.toString(),
|
||||||
sourceType: 'comment_on_lover',
|
sourceType: 'comment_on_profile',
|
||||||
sourceUpdateType: 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUserName: creator.name,
|
sourceUserName: creator.name,
|
||||||
sourceUserUsername: creator.username,
|
sourceUserUsername: creator.username,
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import { updateUser } from 'shared/supabase/users'
|
|||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
import { insert } from 'shared/supabase/utils'
|
import { insert } from 'shared/supabase/utils'
|
||||||
|
|
||||||
export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
|
export const createProfile: APIHandler<'create-profile'> = async (body, auth) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const { data: existingUser } = await tryCatch(
|
const { data: existingUser } = await tryCatch(
|
||||||
pg.oneOrNone<{ id: string }>('select id from lovers where user_id = $1', [
|
pg.oneOrNone<{ id: string }>('select id from profiles where user_id = $1', [
|
||||||
auth.uid,
|
auth.uid,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -31,7 +31,7 @@ export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
|
|||||||
console.log('body', body)
|
console.log('body', body)
|
||||||
|
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
insert(pg, 'lovers', { user_id: auth.uid, ...body })
|
insert(pg, 'profiles', { user_id: auth.uid, ...body })
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -40,7 +40,7 @@ export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log('Created user', data)
|
log('Created user', data)
|
||||||
await track(user.id, 'create lover', { username: user.username })
|
await track(user.id, 'create profile', { username: user.username })
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ export const createUser: APIHandler<'create-user'> = async (
|
|||||||
auth,
|
auth,
|
||||||
req
|
req
|
||||||
) => {
|
) => {
|
||||||
const { deviceToken: preDeviceToken, adminToken } = props
|
const { deviceToken: preDeviceToken } = props
|
||||||
const firebaseUser = await admin.auth().getUser(auth.uid)
|
const firebaseUser = await admin.auth().getUser(auth.uid)
|
||||||
|
|
||||||
const testUserAKAEmailPasswordUser =
|
const testUserAKAEmailPasswordUser =
|
||||||
@@ -123,7 +123,7 @@ export const createUser: APIHandler<'create-user'> = async (
|
|||||||
log('created user ', { username: user.username, firebaseId: auth.uid })
|
log('created user ', { username: user.username, firebaseId: auth.uid })
|
||||||
|
|
||||||
const continuation = async () => {
|
const continuation = async () => {
|
||||||
await track(auth.uid, 'create lover', { username: user.username })
|
await track(auth.uid, 'create profile', { username: user.username })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
`
|
`
|
||||||
select target_id, love_likes.created_time
|
select target_id, love_likes.created_time
|
||||||
from love_likes
|
from love_likes
|
||||||
join lovers on lovers.user_id = love_likes.target_id
|
join profiles on profiles.user_id = love_likes.target_id
|
||||||
join users on users.id = love_likes.target_id
|
join users on users.id = love_likes.target_id
|
||||||
where creator_id = $1
|
where creator_id = $1
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
and lovers.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
||||||
order by created_time desc
|
order by created_time desc
|
||||||
`,
|
`,
|
||||||
@@ -44,11 +44,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
`
|
`
|
||||||
select creator_id, love_likes.created_time
|
select creator_id, love_likes.created_time
|
||||||
from love_likes
|
from love_likes
|
||||||
join lovers on lovers.user_id = love_likes.creator_id
|
join profiles on profiles.user_id = love_likes.creator_id
|
||||||
join users on users.id = love_likes.creator_id
|
join users on users.id = love_likes.creator_id
|
||||||
where target_id = $1
|
where target_id = $1
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
and lovers.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
||||||
order by created_time desc
|
order by created_time desc
|
||||||
`,
|
`,
|
||||||
@@ -71,11 +71,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
target1_id, target2_id, creator_id, love_ships.created_time,
|
target1_id, target2_id, creator_id, love_ships.created_time,
|
||||||
target1_id as target_id
|
target1_id as target_id
|
||||||
from love_ships
|
from love_ships
|
||||||
join lovers on lovers.user_id = love_ships.target1_id
|
join profiles on profiles.user_id = love_ships.target1_id
|
||||||
join users on users.id = love_ships.target1_id
|
join users on users.id = love_ships.target1_id
|
||||||
where target2_id = $1
|
where target2_id = $1
|
||||||
and lovers.looking_for_matches
|
and profiles.looking_for_matches
|
||||||
and lovers.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||||
|
|
||||||
union all
|
union all
|
||||||
@@ -84,11 +84,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
|||||||
target1_id, target2_id, creator_id, love_ships.created_time,
|
target1_id, target2_id, creator_id, love_ships.created_time,
|
||||||
target2_id as target_id
|
target2_id as target_id
|
||||||
from love_ships
|
from love_ships
|
||||||
join lovers on lovers.user_id = love_ships.target2_id
|
join profiles on profiles.user_id = love_ships.target2_id
|
||||||
join users on users.id = love_ships.target2_id
|
join users on users.id = love_ships.target2_id
|
||||||
where target1_id = $1
|
where target1_id = $1
|
||||||
and lovers.looking_for_matches
|
and profiles.looking_for_matches
|
||||||
and lovers.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||||
`,
|
`,
|
||||||
[userId],
|
[userId],
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { type APIHandler } from 'api/helpers/endpoint'
|
|
||||||
import { convertRow } from 'shared/love/supabase'
|
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
|
||||||
import {
|
|
||||||
from,
|
|
||||||
join,
|
|
||||||
limit,
|
|
||||||
orderBy,
|
|
||||||
renderSql,
|
|
||||||
select,
|
|
||||||
where,
|
|
||||||
} from 'shared/supabase/sql-builder'
|
|
||||||
import { getCompatibleLovers } from 'api/compatible-lovers'
|
|
||||||
import { intersection } from 'lodash'
|
|
||||||
|
|
||||||
export const getLovers: APIHandler<'get-lovers'> = async (props, _auth) => {
|
|
||||||
const pg = createSupabaseDirectClient()
|
|
||||||
const {
|
|
||||||
limit: limitParam,
|
|
||||||
after,
|
|
||||||
name,
|
|
||||||
genders,
|
|
||||||
pref_gender,
|
|
||||||
pref_age_min,
|
|
||||||
pref_age_max,
|
|
||||||
pref_relation_styles,
|
|
||||||
wants_kids_strength,
|
|
||||||
has_kids,
|
|
||||||
is_smoker,
|
|
||||||
geodbCityIds,
|
|
||||||
compatibleWithUserId,
|
|
||||||
orderBy: orderByParam,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
// compatibility. TODO: do this in sql
|
|
||||||
if (orderByParam === 'compatibility_score') {
|
|
||||||
if (!compatibleWithUserId) return { status: 'fail', lovers: [] }
|
|
||||||
|
|
||||||
const { compatibleLovers } = await getCompatibleLovers(compatibleWithUserId)
|
|
||||||
const lovers = compatibleLovers.filter(
|
|
||||||
(l) =>
|
|
||||||
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
|
||||||
(!genders || genders.includes(l.gender)) &&
|
|
||||||
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
|
||||||
(!pref_age_min || l.age >= pref_age_min) &&
|
|
||||||
(!pref_age_max || l.age <= pref_age_max) &&
|
|
||||||
(!pref_relation_styles ||
|
|
||||||
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
|
|
||||||
(!wants_kids_strength ||
|
|
||||||
wants_kids_strength == -1 ||
|
|
||||||
(wants_kids_strength >= 2
|
|
||||||
? l.wants_kids_strength >= wants_kids_strength
|
|
||||||
: l.wants_kids_strength <= wants_kids_strength)) &&
|
|
||||||
(has_kids == undefined ||
|
|
||||||
has_kids == -1 ||
|
|
||||||
(has_kids == 0 && !l.has_kids) ||
|
|
||||||
(l.has_kids && l.has_kids > 0)) &&
|
|
||||||
(!is_smoker || l.is_smoker === is_smoker) &&
|
|
||||||
(!geodbCityIds ||
|
|
||||||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id)))
|
|
||||||
)
|
|
||||||
|
|
||||||
const cursor = after
|
|
||||||
? lovers.findIndex((l) => l.id.toString() === after) + 1
|
|
||||||
: 0
|
|
||||||
console.log(cursor)
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
lovers: lovers.slice(cursor, cursor + limitParam),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = renderSql(
|
|
||||||
select('lovers.*, name, username, users.data as user'),
|
|
||||||
from('lovers'),
|
|
||||||
join('users on users.id = lovers.user_id'),
|
|
||||||
where('looking_for_matches = true'),
|
|
||||||
// where(`pinned_url is not null and pinned_url != ''`),
|
|
||||||
where(
|
|
||||||
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
|
|
||||||
),
|
|
||||||
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
|
||||||
|
|
||||||
name &&
|
|
||||||
where(`lower(users.name) ilike '%' || lower($(name)) || '%'`, { name }),
|
|
||||||
|
|
||||||
genders?.length && where(`gender = ANY($(gender))`, { gender: genders }),
|
|
||||||
|
|
||||||
pref_gender?.length &&
|
|
||||||
where(`pref_gender && $(pref_gender)`, { pref_gender }),
|
|
||||||
|
|
||||||
pref_age_min !== undefined &&
|
|
||||||
where(`age >= $(pref_age_min)`, { pref_age_min }),
|
|
||||||
|
|
||||||
pref_age_max !== undefined &&
|
|
||||||
where(`age <= $(pref_age_max)`, { pref_age_max }),
|
|
||||||
|
|
||||||
pref_relation_styles?.length &&
|
|
||||||
where(`pref_relation_styles && $(pref_relation_styles)`, {
|
|
||||||
pref_relation_styles,
|
|
||||||
}),
|
|
||||||
|
|
||||||
wants_kids_strength !== undefined &&
|
|
||||||
wants_kids_strength !== -1 &&
|
|
||||||
where(
|
|
||||||
wants_kids_strength >= 2
|
|
||||||
? `wants_kids_strength >= $(wants_kids_strength)`
|
|
||||||
: `wants_kids_strength <= $(wants_kids_strength)`,
|
|
||||||
{ wants_kids_strength }
|
|
||||||
),
|
|
||||||
|
|
||||||
has_kids === 0 && where(`has_kids IS NULL OR has_kids = 0`),
|
|
||||||
has_kids && has_kids > 0 && where(`has_kids > 0`),
|
|
||||||
|
|
||||||
is_smoker !== undefined && where(`is_smoker = $(is_smoker)`, { is_smoker }),
|
|
||||||
|
|
||||||
geodbCityIds?.length &&
|
|
||||||
where(`geodb_city_id = ANY($(geodbCityIds))`, { geodbCityIds }),
|
|
||||||
|
|
||||||
orderBy(`${orderByParam} desc`),
|
|
||||||
after &&
|
|
||||||
where(
|
|
||||||
`lovers.${orderByParam} < (select lovers.${orderByParam} from lovers where id = $(after))`,
|
|
||||||
{ after }
|
|
||||||
),
|
|
||||||
|
|
||||||
limit(limitParam)
|
|
||||||
)
|
|
||||||
|
|
||||||
const lovers = await pg.map(query, [], convertRow)
|
|
||||||
|
|
||||||
return { status: 'success', lovers: lovers }
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { type APIHandler } from 'api/helpers/endpoint'
|
|||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
|
|
||||||
export const getLoverAnswers: APIHandler<'get-lover-answers'> = async (
|
export const getProfileAnswers: APIHandler<'get-profile-answers'> = async (
|
||||||
props,
|
props,
|
||||||
_auth
|
_auth
|
||||||
) => {
|
) => {
|
||||||
173
backend/api/src/get-profiles.ts
Normal file
173
backend/api/src/get-profiles.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import {type APIHandler} from 'api/helpers/endpoint'
|
||||||
|
import {convertRow} from 'shared/love/supabase'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {from, join, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
|
||||||
|
import {getCompatibleProfiles} from 'api/compatible-profiles'
|
||||||
|
import {intersection} from 'lodash'
|
||||||
|
import {MAX_INT, MIN_INT} from "common/constants";
|
||||||
|
|
||||||
|
export type profileQueryType = {
|
||||||
|
limit?: number | undefined,
|
||||||
|
after?: string | undefined,
|
||||||
|
// Search and filter parameters
|
||||||
|
name?: string | undefined,
|
||||||
|
genders?: String[] | undefined,
|
||||||
|
pref_gender?: String[] | undefined,
|
||||||
|
pref_age_min?: number | undefined,
|
||||||
|
pref_age_max?: number | undefined,
|
||||||
|
pref_relation_styles?: String[] | undefined,
|
||||||
|
wants_kids_strength?: number | undefined,
|
||||||
|
has_kids?: number | undefined,
|
||||||
|
is_smoker?: boolean | undefined,
|
||||||
|
geodbCityIds?: String[] | undefined,
|
||||||
|
compatibleWithUserId?: string | undefined,
|
||||||
|
skipId?: string | undefined,
|
||||||
|
orderBy?: string | undefined,
|
||||||
|
lastModificationWithin?: string | undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const loadProfiles = async (props: profileQueryType) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
console.log(props)
|
||||||
|
const {
|
||||||
|
limit: limitParam,
|
||||||
|
after,
|
||||||
|
name,
|
||||||
|
genders,
|
||||||
|
pref_gender,
|
||||||
|
pref_age_min,
|
||||||
|
pref_age_max,
|
||||||
|
pref_relation_styles,
|
||||||
|
wants_kids_strength,
|
||||||
|
has_kids,
|
||||||
|
is_smoker,
|
||||||
|
geodbCityIds,
|
||||||
|
compatibleWithUserId,
|
||||||
|
orderBy: orderByParam = 'created_time',
|
||||||
|
lastModificationWithin,
|
||||||
|
skipId,
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const keywords = name ? name.split(",").map(q => q.trim()).filter(Boolean) : []
|
||||||
|
// console.debug('keywords:', keywords)
|
||||||
|
|
||||||
|
// compatibility. TODO: do this in sql
|
||||||
|
if (orderByParam === 'compatibility_score') {
|
||||||
|
if (!compatibleWithUserId) {
|
||||||
|
console.error('Incompatible with user ID')
|
||||||
|
throw Error('Incompatible with user ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
const {compatibleProfiles} = await getCompatibleProfiles(compatibleWithUserId)
|
||||||
|
const profiles = compatibleProfiles.filter(
|
||||||
|
(l) =>
|
||||||
|
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
||||||
|
(!genders || genders.includes(l.gender)) &&
|
||||||
|
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
||||||
|
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
|
||||||
|
(!pref_age_max || (l.age ?? MIN_INT) <= pref_age_max) &&
|
||||||
|
(!pref_relation_styles ||
|
||||||
|
intersection(pref_relation_styles, l.pref_relation_styles).length) &&
|
||||||
|
(!wants_kids_strength ||
|
||||||
|
wants_kids_strength == -1 ||
|
||||||
|
(wants_kids_strength >= 2
|
||||||
|
? l.wants_kids_strength >= wants_kids_strength
|
||||||
|
: l.wants_kids_strength <= wants_kids_strength)) &&
|
||||||
|
(has_kids == undefined ||
|
||||||
|
has_kids == -1 ||
|
||||||
|
(has_kids == 0 && !l.has_kids) ||
|
||||||
|
(l.has_kids && l.has_kids > 0)) &&
|
||||||
|
(!is_smoker || l.is_smoker === is_smoker) &&
|
||||||
|
(l.id.toString() != skipId) &&
|
||||||
|
(!geodbCityIds ||
|
||||||
|
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id)))
|
||||||
|
)
|
||||||
|
|
||||||
|
const cursor = after
|
||||||
|
? profiles.findIndex((l) => l.id.toString() === after) + 1
|
||||||
|
: 0
|
||||||
|
console.log(cursor)
|
||||||
|
|
||||||
|
if (limitParam) return profiles.slice(cursor, cursor + limitParam)
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = renderSql(
|
||||||
|
select('profiles.*, name, username, users.data as user'),
|
||||||
|
from('profiles'),
|
||||||
|
join('users on users.id = profiles.user_id'),
|
||||||
|
where('looking_for_matches = true'),
|
||||||
|
// where(`pinned_url is not null and pinned_url != ''`),
|
||||||
|
where(
|
||||||
|
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
|
||||||
|
),
|
||||||
|
where(`data->>'userDeleted' != 'true' or data->>'userDeleted' is null`),
|
||||||
|
|
||||||
|
...keywords.map(word => where(
|
||||||
|
`lower(users.name) ilike '%' || lower($(word)) || '%' or lower(bio::text) ilike '%' || lower($(word)) || '%'`,
|
||||||
|
{word}
|
||||||
|
)),
|
||||||
|
|
||||||
|
genders?.length && where(`gender = ANY($(gender))`, {gender: genders}),
|
||||||
|
|
||||||
|
pref_gender?.length &&
|
||||||
|
where(`pref_gender && $(pref_gender)`, {pref_gender}),
|
||||||
|
|
||||||
|
pref_age_min &&
|
||||||
|
where(`age >= $(pref_age_min) or age is null`, {pref_age_min}),
|
||||||
|
|
||||||
|
pref_age_max &&
|
||||||
|
where(`age <= $(pref_age_max) or age is null`, {pref_age_max}),
|
||||||
|
|
||||||
|
pref_relation_styles?.length &&
|
||||||
|
where(
|
||||||
|
`pref_relation_styles IS NULL OR pref_relation_styles = '{}' OR pref_relation_styles && $(pref_relation_styles)`,
|
||||||
|
{ pref_relation_styles }
|
||||||
|
),
|
||||||
|
|
||||||
|
!!wants_kids_strength &&
|
||||||
|
wants_kids_strength !== -1 &&
|
||||||
|
where(
|
||||||
|
wants_kids_strength >= 2
|
||||||
|
? `wants_kids_strength >= $(wants_kids_strength)`
|
||||||
|
: `wants_kids_strength <= $(wants_kids_strength)`,
|
||||||
|
{wants_kids_strength}
|
||||||
|
),
|
||||||
|
|
||||||
|
has_kids === 0 && where(`has_kids IS NULL OR has_kids = 0`),
|
||||||
|
has_kids && has_kids > 0 && where(`has_kids > 0`),
|
||||||
|
|
||||||
|
is_smoker !== undefined && where(`is_smoker = $(is_smoker)`, {is_smoker}),
|
||||||
|
|
||||||
|
geodbCityIds?.length &&
|
||||||
|
where(`geodb_city_id = ANY($(geodbCityIds))`, {geodbCityIds}),
|
||||||
|
|
||||||
|
skipId && where(`user_id != $(skipId)`, {skipId}),
|
||||||
|
|
||||||
|
orderBy(`${orderByParam} desc`),
|
||||||
|
after &&
|
||||||
|
where(
|
||||||
|
`profiles.${orderByParam} < (select profiles.${orderByParam} from profiles where id = $(after))`,
|
||||||
|
{after}
|
||||||
|
),
|
||||||
|
|
||||||
|
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
|
||||||
|
|
||||||
|
limitParam && limit(limitParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
// console.log('query:', query)
|
||||||
|
|
||||||
|
return await pg.map(query, [], convertRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProfiles: APIHandler<'get-profiles'> = async (props, _auth) => {
|
||||||
|
try {
|
||||||
|
const profiles = await loadProfiles(props)
|
||||||
|
return {status: 'success', profiles: profiles}
|
||||||
|
} catch {
|
||||||
|
return {status: 'fail', profiles: []}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ export const hideComment: APIHandler<'hide-comment'> = async (
|
|||||||
auth
|
auth
|
||||||
) => {
|
) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const comment = await pg.oneOrNone<Row<'lover_comments'>>(
|
const comment = await pg.oneOrNone<Row<'profile_comments'>>(
|
||||||
`select * from lover_comments where id = $1`,
|
`select * from profile_comments where id = $1`,
|
||||||
[commentId]
|
[commentId]
|
||||||
)
|
)
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
@@ -26,7 +26,7 @@ export const hideComment: APIHandler<'hide-comment'> = async (
|
|||||||
throw new APIError(403, 'You are not allowed to hide this comment')
|
throw new APIError(403, 'You are not allowed to hide this comment')
|
||||||
}
|
}
|
||||||
|
|
||||||
await pg.none(`update lover_comments set hidden = $2 where id = $1`, [
|
await pg.none(`update profile_comments set hidden = $2 where id = $1`, [
|
||||||
commentId,
|
commentId,
|
||||||
hide,
|
hide,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { log } from 'shared/utils'
|
|||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
|
|
||||||
export const likeLover: APIHandler<'like-lover'> = async (props, auth) => {
|
export const likeProfile: APIHandler<'like-profile'> = async (props, auth) => {
|
||||||
const { targetUserId, remove } = props
|
const { targetUserId, remove } = props
|
||||||
const creatorId = auth.uid
|
const creatorId = auth.uid
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async (
|
|||||||
|
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
pg.none('update lovers set pinned_url = null where user_id = $1', [userId])
|
pg.none('update profiles set pinned_url = null where user_id = $1', [userId])
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -1,36 +1,8 @@
|
|||||||
import { APIHandler } from './helpers/endpoint'
|
import {APIHandler} from './helpers/endpoint'
|
||||||
|
import {geodbFetch} from "common/geodb";
|
||||||
|
|
||||||
export const searchLocation: APIHandler<'search-location'> = async (body) => {
|
export const searchLocation: APIHandler<'search-location'> = async (body) => {
|
||||||
const { term, limit } = body
|
const {term, limit} = body
|
||||||
const apiKey = process.env.GEODB_API_KEY
|
const endpoint = `/cities?namePrefix=${term}&limit=${limit ?? 10}&offset=0&sort=-population`
|
||||||
console.log('GEODB_API_KEY', apiKey)
|
return await geodbFetch(endpoint)
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return { status: 'failure', data: 'Missing GEODB API key' }
|
|
||||||
}
|
|
||||||
const host = 'wft-geo-db.p.rapidapi.com'
|
|
||||||
const baseUrl = `https://${host}/v1/geo`
|
|
||||||
const url = `${baseUrl}/cities?namePrefix=${term}&limit=${
|
|
||||||
limit ?? 10
|
|
||||||
}&offset=0&sort=-population`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'X-RapidAPI-Key': apiKey,
|
|
||||||
'X-RapidAPI-Host': host,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${res.status} ${await res.text()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
// console.log('GEO DB', data)
|
|
||||||
return { status: 'success', data: data }
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('failure', error)
|
|
||||||
return { status: 'failure', data: error.message }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,17 @@
|
|||||||
import { APIHandler } from './helpers/endpoint'
|
import {APIHandler} from './helpers/endpoint'
|
||||||
|
import {geodbFetch} from "common/geodb";
|
||||||
|
|
||||||
|
const searchNearCityMain = async (cityId: string, radius: number) => {
|
||||||
|
// Limit to 10 cities for now for free plan, was 100 before (may need to buy plan)
|
||||||
|
const endpoint = `/cities/${cityId}/nearbyCities?radius=${radius}&offset=0&sort=-population&limit=10`
|
||||||
|
return await geodbFetch(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
export const searchNearCity: APIHandler<'search-near-city'> = async (body) => {
|
export const searchNearCity: APIHandler<'search-near-city'> = async (body) => {
|
||||||
const { cityId, radius } = body
|
const { cityId, radius } = body
|
||||||
return await searchNearCityMain(cityId, radius)
|
return await searchNearCityMain(cityId, radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchNearCityMain = async (cityId: string, radius: number) => {
|
|
||||||
const apiKey = process.env.GEODB_API_KEY
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return { status: 'failure', data: 'Missing GEODB API key' }
|
|
||||||
}
|
|
||||||
const host = 'wft-geo-db.p.rapidapi.com'
|
|
||||||
const baseUrl = `https://${host}/v1/geo`
|
|
||||||
const url = `${baseUrl}/cities/${cityId}/nearbyCities?radius=${radius}&offset=0&sort=-population&limit=100`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'X-RapidAPI-Key': apiKey,
|
|
||||||
'X-RapidAPI-Host': host,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${res.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
return { status: 'success', data: data }
|
|
||||||
} catch (error) {
|
|
||||||
return { status: 'failure', data: error }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getNearbyCities = async (cityId: string, radius: number) => {
|
export const getNearbyCities = async (cityId: string, radius: number) => {
|
||||||
const result = await searchNearCityMain(cityId, radius)
|
const result = await searchNearCityMain(cityId, radius)
|
||||||
const cityIds = (result.data.data as any[]).map(
|
const cityIds = (result.data.data as any[]).map(
|
||||||
|
|||||||
81
backend/api/src/send-search-notifications.ts
Normal file
81
backend/api/src/send-search-notifications.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {createSupabaseDirectClient} from "shared/supabase/init";
|
||||||
|
import {from, renderSql, select} from "shared/supabase/sql-builder";
|
||||||
|
import {loadProfiles, profileQueryType} from "api/get-profiles";
|
||||||
|
import {Row} from "common/supabase/utils";
|
||||||
|
import {sendSearchAlertsEmail} from "email/functions/helpers";
|
||||||
|
import {MatchesByUserType} from "common/love/bookmarked_searches";
|
||||||
|
import {keyBy} from "lodash";
|
||||||
|
|
||||||
|
export function convertSearchRow(row: any): any {
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const notifyBookmarkedSearch = async (matches: MatchesByUserType) => {
|
||||||
|
for (const [_, value] of Object.entries(matches)) {
|
||||||
|
await sendSearchAlertsEmail(value.user, value.privateUser, value.matches)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendSearchNotifications = async () => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
const search_query = renderSql(
|
||||||
|
select('bookmarked_searches.*'),
|
||||||
|
from('bookmarked_searches'),
|
||||||
|
)
|
||||||
|
const searches = await pg.map(search_query, [], convertSearchRow) as Row<'bookmarked_searches'>[]
|
||||||
|
console.log(`Running ${searches.length} bookmarked searches`)
|
||||||
|
|
||||||
|
const _users = await pg.map(
|
||||||
|
renderSql(
|
||||||
|
select('users.*'),
|
||||||
|
from('users'),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
convertSearchRow
|
||||||
|
) as Row<'users'>[]
|
||||||
|
const users = keyBy(_users, 'id')
|
||||||
|
console.log('users', users)
|
||||||
|
|
||||||
|
const _privateUsers = await pg.map(
|
||||||
|
renderSql(
|
||||||
|
select('private_users.*'),
|
||||||
|
from('private_users'),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
convertSearchRow
|
||||||
|
) as Row<'private_users'>[]
|
||||||
|
const privateUsers = keyBy(_privateUsers, 'id')
|
||||||
|
console.log('privateUsers', privateUsers)
|
||||||
|
|
||||||
|
const matches: MatchesByUserType = {}
|
||||||
|
|
||||||
|
for (const row of searches) {
|
||||||
|
if (typeof row.search_filters !== 'object') continue;
|
||||||
|
const props = {...row.search_filters, skipId: row.creator_id, lastModificationWithin: '24 hours'}
|
||||||
|
const profiles = await loadProfiles(props as profileQueryType)
|
||||||
|
console.log(profiles.map((item: any) => item.name))
|
||||||
|
if (!profiles.length) continue
|
||||||
|
if (!(row.creator_id in matches)) {
|
||||||
|
if (!privateUsers[row.creator_id]) continue
|
||||||
|
matches[row.creator_id] = {
|
||||||
|
user: users[row.creator_id],
|
||||||
|
privateUser: privateUsers[row.creator_id]['data'],
|
||||||
|
matches: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches[row.creator_id].matches.push({
|
||||||
|
id: row.creator_id,
|
||||||
|
description: {filters: row.search_filters, location: row.location},
|
||||||
|
matches: profiles.map((item: any) => ({
|
||||||
|
name: item.name,
|
||||||
|
username: item.username,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log('matches:', JSON.stringify(matches, null, 2))
|
||||||
|
await notifyBookmarkedSearch(matches)
|
||||||
|
|
||||||
|
return {status: 'success'}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { getLocalEnv, initAdmin } from 'shared/init-admin'
|
import {getLocalEnv, initAdmin} from 'shared/init-admin'
|
||||||
import { loadSecretsToEnv, getServiceAccountCredentials } from 'common/secrets'
|
import {loadSecretsToEnv, getServiceAccountCredentials} from 'common/secrets'
|
||||||
import { LOCAL_DEV, log } from 'shared/utils'
|
import {log} from 'shared/utils'
|
||||||
import { METRIC_WRITER } from 'shared/monitoring/metric-writer'
|
import {LOCAL_DEV} from "common/envs/constants";
|
||||||
import { listen as webSocketListen } from 'shared/websockets/server'
|
import {METRIC_WRITER} from 'shared/monitoring/metric-writer'
|
||||||
|
import {listen as webSocketListen} from 'shared/websockets/server'
|
||||||
|
|
||||||
log('Api server starting up....')
|
log('Api server starting up....')
|
||||||
|
|
||||||
@@ -19,12 +20,12 @@ if (LOCAL_DEV) {
|
|||||||
|
|
||||||
METRIC_WRITER.start()
|
METRIC_WRITER.start()
|
||||||
|
|
||||||
import { app } from './app'
|
import {app} from './app'
|
||||||
|
|
||||||
const credentials = LOCAL_DEV
|
const credentials = LOCAL_DEV
|
||||||
? getServiceAccountCredentials(getLocalEnv())
|
? getServiceAccountCredentials(getLocalEnv())
|
||||||
: // No explicit credentials needed for deployed service.
|
: // No explicit credentials needed for deployed service.
|
||||||
undefined
|
undefined
|
||||||
|
|
||||||
const startupProcess = async () => {
|
const startupProcess = async () => {
|
||||||
await loadSecretsToEnv(credentials)
|
await loadSecretsToEnv(credentials)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { log } from 'shared/utils'
|
|||||||
import { tryCatch } from 'common/util/try-catch'
|
import { tryCatch } from 'common/util/try-catch'
|
||||||
import { insert } from 'shared/supabase/utils'
|
import { insert } from 'shared/supabase/utils'
|
||||||
|
|
||||||
export const shipLovers: APIHandler<'ship-lovers'> = async (props, auth) => {
|
export const shipProfiles: APIHandler<'ship-profiles'> = async (props, auth) => {
|
||||||
const { targetUserId1, targetUserId2, remove } = props
|
const { targetUserId1, targetUserId2, remove } = props
|
||||||
const creatorId = auth.uid
|
const creatorId = auth.uid
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ import { tryCatch } from 'common/util/try-catch'
|
|||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
import { insert } from 'shared/supabase/utils'
|
import { insert } from 'shared/supabase/utils'
|
||||||
|
|
||||||
export const starLover: APIHandler<'star-lover'> = async (props, auth) => {
|
export const starProfile: APIHandler<'star-profile'> = async (props, auth) => {
|
||||||
const { targetUserId, remove } = props
|
const { targetUserId, remove } = props
|
||||||
const creatorId = auth.uid
|
const creatorId = auth.uid
|
||||||
|
|
||||||
@@ -7,25 +7,25 @@ import { tryCatch } from 'common/util/try-catch'
|
|||||||
import { update } from 'shared/supabase/utils'
|
import { update } from 'shared/supabase/utils'
|
||||||
import { type Row } from 'common/supabase/utils'
|
import { type Row } from 'common/supabase/utils'
|
||||||
|
|
||||||
export const updateLover: APIHandler<'update-lover'> = async (
|
export const updateProfile: APIHandler<'update-profile'> = async (
|
||||||
parsedBody,
|
parsedBody,
|
||||||
auth
|
auth
|
||||||
) => {
|
) => {
|
||||||
log('parsedBody', parsedBody)
|
log('parsedBody', parsedBody)
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const { data: existingLover } = await tryCatch(
|
const { data: existingProfile } = await tryCatch(
|
||||||
pg.oneOrNone<Row<'lovers'>>('select * from lovers where user_id = $1', [
|
pg.oneOrNone<Row<'profiles'>>('select * from profiles where user_id = $1', [
|
||||||
auth.uid,
|
auth.uid,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!existingLover) {
|
if (!existingProfile) {
|
||||||
throw new APIError(404, 'Lover not found')
|
throw new APIError(404, 'Profile not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
!parsedBody.last_online_time &&
|
!parsedBody.last_online_time &&
|
||||||
log('Updating lover', { userId: auth.uid, parsedBody })
|
log('Updating profile', { userId: auth.uid, parsedBody })
|
||||||
|
|
||||||
await removePinnedUrlFromPhotoUrls(parsedBody)
|
await removePinnedUrlFromPhotoUrls(parsedBody)
|
||||||
if (parsedBody.avatar_url) {
|
if (parsedBody.avatar_url) {
|
||||||
@@ -33,12 +33,12 @@ export const updateLover: APIHandler<'update-lover'> = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
update(pg, 'lovers', 'user_id', { user_id: auth.uid, ...parsedBody })
|
update(pg, 'profiles', 'user_id', { user_id: auth.uid, ...parsedBody })
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
log('Error updating lover', error)
|
log('Error updating profile', error)
|
||||||
throw new APIError(500, 'Error updating lover')
|
throw new APIError(500, 'Error updating profile')
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -9,24 +9,47 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": ["../../common/src/*", "../../../common/lib/*"],
|
"common/*": [
|
||||||
"shared/*": ["../shared/src/*", "../../shared/lib/*"],
|
"../../common/src/*",
|
||||||
"email/*": ["../email/emails/*", "../../email/lib/*"],
|
"../../../common/lib/*"
|
||||||
"api/*": ["./src/*"]
|
],
|
||||||
|
"shared/*": [
|
||||||
|
"../shared/src/*",
|
||||||
|
"../../shared/lib/*"
|
||||||
|
],
|
||||||
|
"email/*": [
|
||||||
|
"../email/emails/*",
|
||||||
|
"../../email/lib/*"
|
||||||
|
],
|
||||||
|
"api/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"require": ["tsconfig-paths/register"]
|
"require": [
|
||||||
|
"tsconfig-paths/register"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../common" },
|
{
|
||||||
{ "path": "../shared" },
|
"path": "../../common"
|
||||||
{ "path": "../email" }
|
},
|
||||||
|
{
|
||||||
|
"path": "../shared"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../email"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": ["src/**/*.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"openapi.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,31 @@ A live preview right in your browser so you don't need to keep sending real emai
|
|||||||
First, install the dependencies:
|
First, install the dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm install
|
yarn install
|
||||||
# or
|
|
||||||
yarn
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run the development server:
|
Then, run the development server:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [localhost:3001](http://localhost:3001) with your browser to see the result.
|
||||||
|
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
Right now, I can't make the email server run without breaking the backend API and web, as they require different versions of react.
|
||||||
|
|
||||||
|
To run the email server, temporarily install the deps in this folder. They require react 19.
|
||||||
|
```bash
|
||||||
|
yarn add -D @react-email/preview-server react-email
|
||||||
|
```
|
||||||
|
|
||||||
|
When you are done, reinstall react 18.2 by running `yarn clean-install` at the root so that you can run the backend and web servers again.
|
||||||
|
|
||||||
|
## Useful commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
|
||||||
|
```
|
||||||
@@ -1,41 +1,43 @@
|
|||||||
import { PrivateUser, User } from 'common/user'
|
import {PrivateUser, User} from 'common/user'
|
||||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
import {getNotificationDestinationsForUser} from 'common/user-notification-preferences'
|
||||||
import { sendEmail } from './send-email'
|
import {sendEmail} from './send-email'
|
||||||
import { NewMatchEmail } from '../new-match'
|
import {NewMessageEmail} from '../new-message'
|
||||||
import { NewMessageEmail } from '../new-message'
|
import {NewEndorsementEmail} from '../new-endorsement'
|
||||||
import { NewEndorsementEmail } from '../new-endorsement'
|
import {Test} from '../test'
|
||||||
import { Test } from '../test'
|
import {getProfile} from 'shared/love/supabase'
|
||||||
import { getLover } from 'shared/love/supabase'
|
import { render } from "@react-email/render"
|
||||||
import {renderToStaticMarkup} from "react-dom/server";
|
import {MatchesType} from "common/love/bookmarked_searches";
|
||||||
|
import NewSearchAlertsEmail from "email/new-search_alerts";
|
||||||
|
|
||||||
const from = 'Compass <no-reply@compassmeet.com>'
|
const from = 'Compass <no-reply@compassmeet.com>'
|
||||||
|
|
||||||
export const sendNewMatchEmail = async (
|
// export const sendNewMatchEmail = async (
|
||||||
privateUser: PrivateUser,
|
// privateUser: PrivateUser,
|
||||||
matchedWithUser: User
|
// matchedWithUser: User
|
||||||
) => {
|
// ) => {
|
||||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
// const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
// privateUser,
|
||||||
'new_match'
|
// 'new_match'
|
||||||
)
|
// )
|
||||||
if (!privateUser.email || !sendToEmail) return
|
// if (!privateUser.email || !sendToEmail) return
|
||||||
const lover = await getLover(privateUser.id)
|
// const profile = await getProfile(privateUser.id)
|
||||||
if (!lover) return
|
// if (!profile) return
|
||||||
|
//
|
||||||
return await sendEmail({
|
// return await sendEmail({
|
||||||
from,
|
// from,
|
||||||
subject: `You have a new match!`,
|
// subject: `You have a new match!`,
|
||||||
to: privateUser.email,
|
// to: privateUser.email,
|
||||||
react: (
|
// react: (
|
||||||
<NewMatchEmail
|
// <NewMatchEmail
|
||||||
onUser={lover.user}
|
// onUser={profile.user}
|
||||||
matchedWithUser={matchedWithUser}
|
// email={privateUser.email}
|
||||||
matchedLover={lover}
|
// matchedWithUser={matchedWithUser}
|
||||||
unsubscribeUrl={unsubscribeUrl}
|
// matchedProfile={profile}
|
||||||
/>
|
// unsubscribeUrl={unsubscribeUrl}
|
||||||
),
|
// />
|
||||||
})
|
// ),
|
||||||
}
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
export const sendNewMessageEmail = async (
|
export const sendNewMessageEmail = async (
|
||||||
privateUser: PrivateUser,
|
privateUser: PrivateUser,
|
||||||
@@ -43,45 +45,58 @@ export const sendNewMessageEmail = async (
|
|||||||
toUser: User,
|
toUser: User,
|
||||||
channelId: number
|
channelId: number
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'new_message'
|
'new_message'
|
||||||
)
|
)
|
||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
const lover = await getLover(fromUser.id)
|
const profile = await getProfile(fromUser.id)
|
||||||
|
|
||||||
if (!lover) {
|
if (!profile) {
|
||||||
console.error('Could not send email notification: User not found')
|
console.error('Could not send email notification: User not found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({
|
|
||||||
from,
|
|
||||||
subject: `${fromUser.name} sent you a message!`,
|
|
||||||
to: privateUser.email,
|
|
||||||
html: renderToStaticMarkup(
|
|
||||||
<NewMessageEmail
|
|
||||||
fromUser={fromUser}
|
|
||||||
fromUserLover={lover}
|
|
||||||
toUser={toUser}
|
|
||||||
channelId={channelId}
|
|
||||||
unsubscribeUrl={unsubscribeUrl}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from,
|
||||||
subject: `${fromUser.name} sent you a message!`,
|
subject: `${fromUser.name} sent you a message!`,
|
||||||
to: privateUser.email,
|
to: privateUser.email,
|
||||||
html: renderToStaticMarkup(
|
html: await render(
|
||||||
<NewMessageEmail
|
<NewMessageEmail
|
||||||
fromUser={fromUser}
|
fromUser={fromUser}
|
||||||
fromUserLover={lover}
|
fromUserProfile={profile}
|
||||||
toUser={toUser}
|
toUser={toUser}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
unsubscribeUrl={unsubscribeUrl}
|
unsubscribeUrl={unsubscribeUrl}
|
||||||
|
email={privateUser.email}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendSearchAlertsEmail = async (
|
||||||
|
toUser: User,
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
matches: MatchesType[],
|
||||||
|
) => {
|
||||||
|
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||||
|
privateUser,
|
||||||
|
'new_search_alerts'
|
||||||
|
)
|
||||||
|
const email = privateUser.email;
|
||||||
|
if (!email || !sendToEmail) return
|
||||||
|
|
||||||
|
return await sendEmail({
|
||||||
|
from,
|
||||||
|
subject: `People aligned with your values just joined`,
|
||||||
|
to: email,
|
||||||
|
html: await render(
|
||||||
|
<NewSearchAlertsEmail
|
||||||
|
toUser={toUser}
|
||||||
|
matches={matches}
|
||||||
|
unsubscribeUrl={unsubscribeUrl}
|
||||||
|
email={email}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -93,7 +108,7 @@ export const sendNewEndorsementEmail = async (
|
|||||||
onUser: User,
|
onUser: User,
|
||||||
text: string
|
text: string
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'new_endorsement'
|
'new_endorsement'
|
||||||
)
|
)
|
||||||
@@ -103,12 +118,13 @@ export const sendNewEndorsementEmail = async (
|
|||||||
from,
|
from,
|
||||||
subject: `${fromUser.name} just endorsed you!`,
|
subject: `${fromUser.name} just endorsed you!`,
|
||||||
to: privateUser.email,
|
to: privateUser.email,
|
||||||
react: (
|
html: await render(
|
||||||
<NewEndorsementEmail
|
<NewEndorsementEmail
|
||||||
fromUser={fromUser}
|
fromUser={fromUser}
|
||||||
onUser={onUser}
|
onUser={onUser}
|
||||||
endorsementText={text}
|
endorsementText={text}
|
||||||
unsubscribeUrl={unsubscribeUrl}
|
unsubscribeUrl={unsubscribeUrl}
|
||||||
|
email={privateUser.email}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -119,6 +135,6 @@ export const sendTestEmail = async (toEmail: string) => {
|
|||||||
from,
|
from,
|
||||||
subject: 'Test email from Compass',
|
subject: 'Test email from Compass',
|
||||||
to: toEmail,
|
to: toEmail,
|
||||||
html: renderToStaticMarkup(<Test name="Test User" />),
|
html: await render(<Test name="Test User"/>),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoverRow } from 'common/love/lover'
|
import { ProfileRow } from 'common/love/profile'
|
||||||
import type { User } from 'common/user'
|
import type { User } from 'common/user'
|
||||||
|
|
||||||
// for email template testing
|
// for email template testing
|
||||||
@@ -27,11 +27,12 @@ export const sinclairUser: User = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sinclairLover: LoverRow = {
|
export const sinclairProfile: ProfileRow = {
|
||||||
id: 55,
|
id: 55,
|
||||||
user_id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
user_id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
||||||
created_time: '2023-10-27T00:41:59.851776+00:00',
|
created_time: '2023-10-27T00:41:59.851776+00:00',
|
||||||
last_online_time: '2024-05-17T02:11:48.83+00:00',
|
last_online_time: '2024-05-17T02:11:48.83+00:00',
|
||||||
|
last_modification_time: '2024-05-17T02:11:48.83+00:00',
|
||||||
city: 'San Francisco',
|
city: 'San Francisco',
|
||||||
gender: 'trans-female',
|
gender: 'trans-female',
|
||||||
pref_gender: ['female', 'trans-female'],
|
pref_gender: ['female', 'trans-female'],
|
||||||
@@ -124,11 +125,12 @@ export const jamesUser: User = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jamesLover: LoverRow = {
|
export const jamesProfile: ProfileRow = {
|
||||||
id: 2,
|
id: 2,
|
||||||
user_id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
|
user_id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
|
||||||
created_time: '2023-10-21T21:18:26.691211+00:00',
|
created_time: '2023-10-21T21:18:26.691211+00:00',
|
||||||
last_online_time: '2024-07-06T17:29:16.833+00:00',
|
last_online_time: '2024-07-06T17:29:16.833+00:00',
|
||||||
|
last_modification_time: '2024-05-17T02:11:48.83+00:00',
|
||||||
city: 'San Francisco',
|
city: 'San Francisco',
|
||||||
gender: 'male',
|
gender: 'male',
|
||||||
pref_gender: ['female'],
|
pref_gender: ['female'],
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const getResend = () => {
|
|||||||
if (resend) return resend
|
if (resend) return resend
|
||||||
|
|
||||||
const apiKey = process.env.RESEND_KEY as string
|
const apiKey = process.env.RESEND_KEY as string
|
||||||
console.log(`RESEND_KEY: ${apiKey}`)
|
// console.log(`RESEND_KEY: ${apiKey}`)
|
||||||
resend = new Resend(apiKey)
|
resend = new Resend(apiKey)
|
||||||
return resend
|
return resend
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,31 @@
|
|||||||
import {
|
import {Body, Button, Column, Container, Head, Html, Preview, Row, Section, Text,} from '@react-email/components'
|
||||||
Body,
|
import {type User} from 'common/user'
|
||||||
Button,
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
Container,
|
import {jamesUser, sinclairUser} from './functions/mock'
|
||||||
Column,
|
import {button, container, content, Footer, main, paragraph} from "email/utils";
|
||||||
Head,
|
|
||||||
Html,
|
|
||||||
Img,
|
|
||||||
Link,
|
|
||||||
Preview,
|
|
||||||
Row,
|
|
||||||
Section,
|
|
||||||
Text,
|
|
||||||
} from '@react-email/components'
|
|
||||||
import { type User } from 'common/user'
|
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
|
||||||
import { jamesUser, sinclairUser } from './functions/mock'
|
|
||||||
|
|
||||||
interface NewEndorsementEmailProps {
|
interface NewEndorsementEmailProps {
|
||||||
fromUser: User
|
fromUser: User
|
||||||
onUser: User
|
onUser: User
|
||||||
endorsementText: string
|
endorsementText: string
|
||||||
unsubscribeUrl: string
|
unsubscribeUrl: string
|
||||||
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewEndorsementEmail = ({
|
export const NewEndorsementEmail = ({
|
||||||
fromUser,
|
fromUser,
|
||||||
onUser,
|
onUser,
|
||||||
endorsementText,
|
endorsementText,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
}: NewEndorsementEmailProps) => {
|
email,
|
||||||
|
}: NewEndorsementEmailProps) => {
|
||||||
const name = onUser.name.split(' ')[0]
|
const name = onUser.name.split(' ')[0]
|
||||||
|
|
||||||
const endorsementUrl = `https://${DOMAIN}/${onUser.username}`
|
const endorsementUrl = `https://${DOMAIN}/${onUser.username}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head/>
|
||||||
<Preview>New endorsement from {fromUser.name}</Preview>
|
<Preview>New endorsement from {fromUser.name}</Preview>
|
||||||
<Body style={main}>
|
<Body style={main}>
|
||||||
<Container style={container}>
|
<Container style={container}>
|
||||||
@@ -55,15 +45,15 @@ export const NewEndorsementEmail = ({
|
|||||||
|
|
||||||
<Section style={endorsementContainer}>
|
<Section style={endorsementContainer}>
|
||||||
<Row>
|
<Row>
|
||||||
<Column>
|
{/*<Column>*/}
|
||||||
<Img
|
{/* <Img*/}
|
||||||
src={fromUser.avatarUrl}
|
{/* src={fromUser.avatarUrl}*/}
|
||||||
width="50"
|
{/* width="50"*/}
|
||||||
height="50"
|
{/* height="50"*/}
|
||||||
alt=""
|
{/* alt=""*/}
|
||||||
style={avatarImage}
|
{/* style={avatarImage}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</Column>
|
{/*</Column>*/}
|
||||||
<Column>
|
<Column>
|
||||||
<Text style={endorsementTextStyle}>"{endorsementText}"</Text>
|
<Text style={endorsementTextStyle}>"{endorsementText}"</Text>
|
||||||
</Column>
|
</Column>
|
||||||
@@ -75,15 +65,7 @@ export const NewEndorsementEmail = ({
|
|||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section style={footer}>
|
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||||
<Text style={footerText}>
|
|
||||||
This e-mail has been sent to {name},{' '}
|
|
||||||
{/* <Link href={unsubscribeUrl} style={footerLink}>
|
|
||||||
click here to unsubscribe from this type of notification
|
|
||||||
</Link>
|
|
||||||
. */}
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
@@ -96,37 +78,9 @@ NewEndorsementEmail.PreviewProps = {
|
|||||||
endorsementText:
|
endorsementText:
|
||||||
"Sinclair is someone you want to have around because she injects creativity and humor into every conversation, and her laugh is infectious! Not to mention that she's a great employee, treats everyone with respect, and is even-tempered.",
|
"Sinclair is someone you want to have around because she injects creativity and humor into every conversation, and her laugh is infectious! Not to mention that she's a great employee, treats everyone with respect, and is even-tempered.",
|
||||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||||
|
email: 'someone@gmail.com',
|
||||||
} as NewEndorsementEmailProps
|
} as NewEndorsementEmailProps
|
||||||
|
|
||||||
const main = {
|
|
||||||
backgroundColor: '#f4f4f4',
|
|
||||||
fontFamily: 'Arial, sans-serif',
|
|
||||||
wordSpacing: 'normal',
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
margin: '0 auto',
|
|
||||||
maxWidth: '600px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoContainer = {
|
|
||||||
padding: '20px 0px 5px 0px',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
padding: '20px 25px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const paragraph = {
|
|
||||||
fontSize: '18px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
margin: '10px 0',
|
|
||||||
color: '#000000',
|
|
||||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
||||||
}
|
|
||||||
|
|
||||||
const endorsementContainer = {
|
const endorsementContainer = {
|
||||||
margin: '20px 0',
|
margin: '20px 0',
|
||||||
@@ -135,10 +89,6 @@ const endorsementContainer = {
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarImage = {
|
|
||||||
borderRadius: '50%',
|
|
||||||
}
|
|
||||||
|
|
||||||
const endorsementTextStyle = {
|
const endorsementTextStyle = {
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
@@ -146,35 +96,4 @@ const endorsementTextStyle = {
|
|||||||
color: '#333333',
|
color: '#333333',
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = {
|
|
||||||
backgroundColor: '#4887ec',
|
|
||||||
borderRadius: '12px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'semibold',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '6px 10px',
|
|
||||||
margin: '10px 0',
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = {
|
|
||||||
margin: '20px 0',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerText = {
|
|
||||||
fontSize: '11px',
|
|
||||||
lineHeight: '22px',
|
|
||||||
color: '#000000',
|
|
||||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerLink = {
|
|
||||||
color: 'inherit',
|
|
||||||
textDecoration: 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewEndorsementEmail
|
export default NewEndorsementEmail
|
||||||
|
|||||||
@@ -1,51 +1,43 @@
|
|||||||
import {
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
Body,
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
Button,
|
import {type ProfileRow} from 'common/love/profile'
|
||||||
Container,
|
import {type User} from 'common/user'
|
||||||
Head,
|
import {jamesProfile, jamesUser, sinclairUser} from './functions/mock'
|
||||||
Html,
|
import {Footer} from "email/utils";
|
||||||
Img,
|
|
||||||
Link,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Text,
|
|
||||||
} from '@react-email/components'
|
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
|
||||||
import { type LoverRow } from 'common/love/lover'
|
|
||||||
import { getLoveOgImageUrl } from 'common/love/og-image'
|
|
||||||
import { type User } from 'common/user'
|
|
||||||
import { jamesLover, jamesUser, sinclairUser } from './functions/mock'
|
|
||||||
|
|
||||||
interface NewMatchEmailProps {
|
interface NewMatchEmailProps {
|
||||||
onUser: User
|
onUser: User
|
||||||
matchedWithUser: User
|
matchedWithUser: User
|
||||||
matchedLover: LoverRow
|
matchedProfile: ProfileRow
|
||||||
unsubscribeUrl: string
|
unsubscribeUrl: string
|
||||||
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewMatchEmail = ({
|
export const NewMatchEmail = ({
|
||||||
onUser,
|
onUser,
|
||||||
matchedWithUser,
|
matchedWithUser,
|
||||||
matchedLover,
|
// matchedProfile,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
}: NewMatchEmailProps) => {
|
email
|
||||||
|
}: NewMatchEmailProps) => {
|
||||||
const name = onUser.name.split(' ')[0]
|
const name = onUser.name.split(' ')[0]
|
||||||
const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedLover)
|
// const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedProfile)
|
||||||
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head/>
|
||||||
<Preview>You have a new match!</Preview>
|
<Preview>You have a new match!</Preview>
|
||||||
<Body style={main}>
|
<Body style={main}>
|
||||||
<Container style={container}>
|
<Container style={container}>
|
||||||
|
|
||||||
{/*<Section style={logoContainer}>*/}
|
{/*<Section style={logoContainer}>*/}
|
||||||
{/* <Img*/}
|
{/*<Img*/}
|
||||||
{/* src="..."*/}
|
{/* src="..."*/}
|
||||||
{/* width="550"*/}
|
{/* width="550"*/}
|
||||||
{/* height="auto"*/}
|
{/* height="auto"*/}
|
||||||
{/* alt="compassmeet.com"*/}
|
{/* alt="compassmeet.com"*/}
|
||||||
{/* />*/}
|
{/*/>*/}
|
||||||
{/*</Section>*/}
|
{/*</Section>*/}
|
||||||
|
|
||||||
<Section style={content}>
|
<Section style={content}>
|
||||||
@@ -56,31 +48,21 @@ export const NewMatchEmail = ({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Section style={imageContainer}>
|
<Section style={imageContainer}>
|
||||||
<Link href={userUrl}>
|
{/*<Link href={userUrl}>*/}
|
||||||
<Img
|
{/* <Img*/}
|
||||||
src={userImgSrc}
|
{/* src={userImgSrc}*/}
|
||||||
width="375"
|
{/* width="375"*/}
|
||||||
height="200"
|
{/* height="200"*/}
|
||||||
alt=""
|
{/* alt=""*/}
|
||||||
style={profileImage}
|
{/* style={profileImage}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</Link>
|
{/*</Link>*/}
|
||||||
|
|
||||||
<Button href={userUrl} style={button}>
|
<Button href={userUrl} style={button}>
|
||||||
View profile
|
View profile
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
|
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||||
<Section style={footer}>
|
|
||||||
<Text style={footerText}>
|
|
||||||
This e-mail has been sent to {name},{' '}
|
|
||||||
{/* <Link href={unsubscribeUrl} style={footerLink}>
|
|
||||||
click here to unsubscribe from this type of notification
|
|
||||||
</Link>
|
|
||||||
. */}
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
@@ -90,12 +72,13 @@ export const NewMatchEmail = ({
|
|||||||
NewMatchEmail.PreviewProps = {
|
NewMatchEmail.PreviewProps = {
|
||||||
onUser: sinclairUser,
|
onUser: sinclairUser,
|
||||||
matchedWithUser: jamesUser,
|
matchedWithUser: jamesUser,
|
||||||
matchedLover: jamesLover,
|
matchedProfile: jamesProfile,
|
||||||
|
email: 'someone@gmail.com',
|
||||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||||
} as NewMatchEmailProps
|
} as NewMatchEmailProps
|
||||||
|
|
||||||
const main = {
|
const main = {
|
||||||
backgroundColor: '#f4f4f4',
|
// backgroundColor: '#f4f4f4',
|
||||||
fontFamily: 'Arial, sans-serif',
|
fontFamily: 'Arial, sans-serif',
|
||||||
wordSpacing: 'normal',
|
wordSpacing: 'normal',
|
||||||
}
|
}
|
||||||
@@ -105,11 +88,11 @@ const container = {
|
|||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoContainer = {
|
// const logoContainer = {
|
||||||
padding: '20px 0px 5px 0px',
|
// padding: '20px 0px 5px 0px',
|
||||||
textAlign: 'center' as const,
|
// textAlign: 'center' as const,
|
||||||
backgroundColor: '#ffffff',
|
// backgroundColor: '#ffffff',
|
||||||
}
|
// }
|
||||||
|
|
||||||
const content = {
|
const content = {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
@@ -129,9 +112,9 @@ const imageContainer = {
|
|||||||
margin: '20px 0',
|
margin: '20px 0',
|
||||||
}
|
}
|
||||||
|
|
||||||
const profileImage = {
|
// const profileImage = {
|
||||||
// border: '1px solid #ec489a',
|
// // border: '1px solid #ec489a',
|
||||||
}
|
// }
|
||||||
|
|
||||||
const button = {
|
const button = {
|
||||||
backgroundColor: '#4887ec',
|
backgroundColor: '#4887ec',
|
||||||
@@ -147,21 +130,4 @@ const button = {
|
|||||||
margin: '10px 0',
|
margin: '10px 0',
|
||||||
}
|
}
|
||||||
|
|
||||||
const footer = {
|
|
||||||
margin: '20px 0',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerText = {
|
|
||||||
fontSize: '11px',
|
|
||||||
lineHeight: '22px',
|
|
||||||
color: '#000000',
|
|
||||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerLink = {
|
|
||||||
color: 'inherit',
|
|
||||||
textDecoration: 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewMatchEmail
|
export default NewMatchEmail
|
||||||
|
|||||||
@@ -1,49 +1,35 @@
|
|||||||
import {
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
Body,
|
import {type User} from 'common/user'
|
||||||
Button,
|
import {type ProfileRow} from 'common/love/profile'
|
||||||
Container,
|
import {jamesProfile, jamesUser, sinclairUser,} from './functions/mock'
|
||||||
Head,
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
Html,
|
import {button, container, content, Footer, imageContainer, main, paragraph} from "email/utils";
|
||||||
Img,
|
|
||||||
Link,
|
|
||||||
Preview,
|
|
||||||
Section,
|
|
||||||
Text,
|
|
||||||
} from '@react-email/components'
|
|
||||||
import { type User } from 'common/user'
|
|
||||||
import { type LoverRow } from 'common/love/lover'
|
|
||||||
import {
|
|
||||||
jamesLover,
|
|
||||||
jamesUser,
|
|
||||||
sinclairLover,
|
|
||||||
sinclairUser,
|
|
||||||
} from './functions/mock'
|
|
||||||
import { DOMAIN } from 'common/envs/constants'
|
|
||||||
import { getLoveOgImageUrl } from 'common/love/og-image'
|
|
||||||
|
|
||||||
interface NewMessageEmailProps {
|
interface NewMessageEmailProps {
|
||||||
fromUser: User
|
fromUser: User
|
||||||
fromUserLover: LoverRow
|
fromUserProfile: ProfileRow
|
||||||
toUser: User
|
toUser: User
|
||||||
channelId: number
|
channelId: number
|
||||||
unsubscribeUrl: string
|
unsubscribeUrl: string
|
||||||
|
email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewMessageEmail = ({
|
export const NewMessageEmail = ({
|
||||||
fromUser,
|
fromUser,
|
||||||
fromUserLover,
|
// fromUserProfile,
|
||||||
toUser,
|
toUser,
|
||||||
channelId,
|
channelId,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
}: NewMessageEmailProps) => {
|
email,
|
||||||
|
}: NewMessageEmailProps) => {
|
||||||
const name = toUser.name.split(' ')[0]
|
const name = toUser.name.split(' ')[0]
|
||||||
const creatorName = fromUser.name
|
const creatorName = fromUser.name
|
||||||
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
|
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
|
||||||
const userImgSrc = getLoveOgImageUrl(fromUser, fromUserLover)
|
// const userImgSrc = getLoveOgImageUrl(fromUser, fromUserProfile)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head/>
|
||||||
<Preview>New message from {creatorName}</Preview>
|
<Preview>New message from {creatorName}</Preview>
|
||||||
<Body style={main}>
|
<Body style={main}>
|
||||||
<Container style={container}>
|
<Container style={container}>
|
||||||
@@ -62,15 +48,15 @@ export const NewMessageEmail = ({
|
|||||||
<Text style={paragraph}>{creatorName} just messaged you!</Text>
|
<Text style={paragraph}>{creatorName} just messaged you!</Text>
|
||||||
|
|
||||||
<Section style={imageContainer}>
|
<Section style={imageContainer}>
|
||||||
<Link href={messagesUrl}>
|
{/*<Link href={messagesUrl}>*/}
|
||||||
<Img
|
{/* <Img*/}
|
||||||
src={userImgSrc}
|
{/* src={userImgSrc}*/}
|
||||||
width="375"
|
{/* width="375"*/}
|
||||||
height="200"
|
{/* height="200"*/}
|
||||||
alt={`${creatorName}'s profile`}
|
{/* alt={`${creatorName}'s profile`}*/}
|
||||||
style={profileImage}
|
{/* style={profileImage}*/}
|
||||||
/>
|
{/* />*/}
|
||||||
</Link>
|
{/*</Link>*/}
|
||||||
|
|
||||||
<Button href={messagesUrl} style={button}>
|
<Button href={messagesUrl} style={button}>
|
||||||
View message
|
View message
|
||||||
@@ -78,15 +64,7 @@ export const NewMessageEmail = ({
|
|||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section style={footer}>
|
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||||
<Text style={footerText}>
|
|
||||||
This e-mail has been sent to {name},{' '}
|
|
||||||
{/* <Link href={unsubscribeUrl} style={{ color: 'inherit', textDecoration: 'none' }}>
|
|
||||||
click here to unsubscribe from this type of notification
|
|
||||||
</Link>
|
|
||||||
. */}
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
@@ -95,75 +73,12 @@ export const NewMessageEmail = ({
|
|||||||
|
|
||||||
NewMessageEmail.PreviewProps = {
|
NewMessageEmail.PreviewProps = {
|
||||||
fromUser: jamesUser,
|
fromUser: jamesUser,
|
||||||
fromUserLover: jamesLover,
|
fromUserProfile: jamesProfile,
|
||||||
toUser: sinclairUser,
|
toUser: sinclairUser,
|
||||||
channelId: 1,
|
channelId: 1,
|
||||||
|
email: 'someone@gmail.com',
|
||||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||||
} as NewMessageEmailProps
|
} as NewMessageEmailProps
|
||||||
|
|
||||||
const main = {
|
|
||||||
backgroundColor: '#f4f4f4',
|
|
||||||
fontFamily: 'Arial, sans-serif',
|
|
||||||
wordSpacing: 'normal',
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
margin: '0 auto',
|
|
||||||
maxWidth: '600px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoContainer = {
|
|
||||||
padding: '20px 0px 5px 0px',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
padding: '20px 25px',
|
|
||||||
}
|
|
||||||
|
|
||||||
const paragraph = {
|
|
||||||
fontSize: '18px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
margin: '10px 0',
|
|
||||||
color: '#000000',
|
|
||||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageContainer = {
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
margin: '20px 0',
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileImage = {
|
|
||||||
// border: '1px solid #ec489a',
|
|
||||||
}
|
|
||||||
|
|
||||||
const button = {
|
|
||||||
backgroundColor: '#4887ec',
|
|
||||||
borderRadius: '12px',
|
|
||||||
color: '#ffffff',
|
|
||||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'semibold',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '6px 10px',
|
|
||||||
margin: '10px 0',
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = {
|
|
||||||
margin: '20px 0',
|
|
||||||
textAlign: 'center' as const,
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerText = {
|
|
||||||
fontSize: '11px',
|
|
||||||
lineHeight: '22px',
|
|
||||||
color: '#000000',
|
|
||||||
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewMessageEmail
|
export default NewMessageEmail
|
||||||
|
|||||||
150
backend/email/emails/new-search_alerts.tsx
Normal file
150
backend/email/emails/new-search_alerts.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import {Body, Container, Head, Html, Link, Preview, Section, Text,} from '@react-email/components'
|
||||||
|
import {type User} from 'common/user'
|
||||||
|
import {sinclairUser,} from './functions/mock'
|
||||||
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
|
import {container, content, Footer, main, paragraph} from "email/utils";
|
||||||
|
import {MatchesType} from "common/love/bookmarked_searches";
|
||||||
|
import {formatFilters, locationType} from "common/searches"
|
||||||
|
import {FilterFields} from "common/filters";
|
||||||
|
|
||||||
|
interface NewMessageEmailProps {
|
||||||
|
toUser: User
|
||||||
|
matches: MatchesType[]
|
||||||
|
unsubscribeUrl: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewSearchAlertsEmail = ({
|
||||||
|
toUser,
|
||||||
|
unsubscribeUrl,
|
||||||
|
matches,
|
||||||
|
email,
|
||||||
|
}: NewMessageEmailProps) => {
|
||||||
|
const name = toUser.name.split(' ')[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head/>
|
||||||
|
<Preview>New people share your values — reach out and connect</Preview>
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Section style={content}>
|
||||||
|
<Text style={paragraph}>Hi {name},</Text>
|
||||||
|
|
||||||
|
<Text style={paragraph}>
|
||||||
|
In the past 24 hours, new people joined Compass whose values and
|
||||||
|
interests align with your saved searches. Compass is a gift from the
|
||||||
|
community, and it comes alive when people like you take the step to
|
||||||
|
connect with one another.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{(matches || []).map((match) => (
|
||||||
|
<Section key={match.id} style={{marginBottom: "20px"}}>
|
||||||
|
<Text style={{fontWeight: "bold", marginBottom: "5px"}}>
|
||||||
|
{formatFilters(
|
||||||
|
match.description.filters as Partial<FilterFields>,
|
||||||
|
match.description.location as locationType
|
||||||
|
)?.join(" • ")}
|
||||||
|
</Text>
|
||||||
|
<Text style={{margin: 0}}>
|
||||||
|
{match.matches.map((p, i) => (
|
||||||
|
<span key={p.username}>
|
||||||
|
{p.name} (
|
||||||
|
<Link
|
||||||
|
href={`https://${DOMAIN}/${p.username}`}
|
||||||
|
style={{color: "#2563eb", textDecoration: "none"}}
|
||||||
|
>
|
||||||
|
@{p.username}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
{i < match.matches.length - 1 && ", "}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Section style={{textAlign: "center", marginTop: "30px"}}>
|
||||||
|
<Text style={{marginBottom: "20px"}}>
|
||||||
|
If someone resonates with you, reach out. A simple hello can be the
|
||||||
|
start of a meaningful friendship, collaboration, or relationship.
|
||||||
|
</Text>
|
||||||
|
<Link
|
||||||
|
href={`https://${DOMAIN}/messages`}
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
backgroundColor: "#2563eb",
|
||||||
|
color: "#ffffff",
|
||||||
|
padding: "12px 20px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
textDecoration: "none",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start a Conversation
|
||||||
|
</Link>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text style={{marginTop: "40px", fontSize: "12px", color: "#555"}}>
|
||||||
|
Compass is built and sustained by the community — no ads, no hidden
|
||||||
|
algorithms, no subscriptions. Your presence and participation make it
|
||||||
|
possible.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Footer unsubscribeUrl={unsubscribeUrl} email={email ?? name}/>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchSamples = [
|
||||||
|
{
|
||||||
|
"id": "ID search 1",
|
||||||
|
"description": {
|
||||||
|
"filters": {
|
||||||
|
"orderBy": "created_time"
|
||||||
|
},
|
||||||
|
"location": null
|
||||||
|
},
|
||||||
|
"matches": [
|
||||||
|
{
|
||||||
|
"name": "James Bond",
|
||||||
|
"username": "jamesbond"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lily",
|
||||||
|
"username": "lilyrose"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ID search 2",
|
||||||
|
"description": {
|
||||||
|
"filters": {
|
||||||
|
"genders": [
|
||||||
|
"female"
|
||||||
|
],
|
||||||
|
"orderBy": "created_time"
|
||||||
|
},
|
||||||
|
"location": null
|
||||||
|
},
|
||||||
|
"matches": [
|
||||||
|
{
|
||||||
|
"name": "Lily",
|
||||||
|
"username": "lilyrose"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
NewSearchAlertsEmail.PreviewProps = {
|
||||||
|
toUser: sinclairUser,
|
||||||
|
email: 'someone@gmail.com',
|
||||||
|
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||||
|
matches: matchSamples,
|
||||||
|
} as NewMessageEmailProps
|
||||||
|
|
||||||
|
|
||||||
|
export default NewSearchAlertsEmail
|
||||||
@@ -16,7 +16,7 @@ export const Test = (props: { name: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Test.PreviewProps = {
|
Test.PreviewProps = {
|
||||||
name: 'Clarity',
|
name: 'Friend',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Test
|
export default Test
|
||||||
|
|||||||
145
backend/email/emails/utils.tsx
Normal file
145
backend/email/emails/utils.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {Column, Img, Link, Row, Section, Text} from "@react-email/components";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
email?: string
|
||||||
|
unsubscribeUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footer = ({
|
||||||
|
email,
|
||||||
|
unsubscribeUrl,
|
||||||
|
}: Props) => {
|
||||||
|
return <Section style={footer}>
|
||||||
|
<hr style={{border: 'none', borderTop: '1px solid #e0e0e0', margin: '10px 0'}}/>
|
||||||
|
<Row>
|
||||||
|
<Column align="center">
|
||||||
|
<Link href="https://github.com/CompassConnections/Compass" target="_blank">
|
||||||
|
<Img
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/733/733553.png"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
alt="GitHub"
|
||||||
|
style={{ display: "inline-block", margin: "0 4px" }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<Link href="https://discord.gg/8Vd7jzqjun" target="_blank">
|
||||||
|
<Img
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/2111/2111370.png"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
alt="Discord"
|
||||||
|
style={{ display: "inline-block", margin: "0 4px" }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<Link href="https://patreon.com/CompassMeet" target="_blank">
|
||||||
|
<Img
|
||||||
|
src="https://static.vecteezy.com/system/resources/previews/027/127/454/non_2x/patreon-logo-patreon-icon-transparent-free-png.png"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
alt="Patreon"
|
||||||
|
style={{ display: "inline-block", margin: "0 4px" }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<Link href="https://www.paypal.com/paypalme/CompassConnections" target="_blank">
|
||||||
|
<Img
|
||||||
|
src="https://cdn-icons-png.flaticon.com/512/174/174861.png"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
alt="PayPal"
|
||||||
|
style={{ display: "inline-block", margin: "0 4px" }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Text style={{fontSize: "12px", color: "#888", marginTop: "12px"}}>
|
||||||
|
© {new Date().getFullYear()} Compass
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={{fontSize: "10px", color: "#888", marginTop: "12px"}}>
|
||||||
|
The email was sent to {email}. To no longer receive these emails, unsubscribe {' '}
|
||||||
|
<Link href={unsubscribeUrl}>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const footer = {
|
||||||
|
margin: '20px 0',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const footerText = {
|
||||||
|
fontSize: '11px',
|
||||||
|
lineHeight: '22px',
|
||||||
|
color: '#000000',
|
||||||
|
fontFamily: 'Ubuntu, Helvetica, Arial, sans-serif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blackLinks = {
|
||||||
|
color: 'black'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// const footerLink = {
|
||||||
|
// color: 'inherit',
|
||||||
|
// textDecoration: 'none',
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
export const main = {
|
||||||
|
// backgroundColor: '#f4f4f4',
|
||||||
|
fontFamily: 'Arial, sans-serif',
|
||||||
|
wordSpacing: 'normal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const container = {
|
||||||
|
margin: '0 auto',
|
||||||
|
maxWidth: '600px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logoContainer = {
|
||||||
|
padding: '20px 0px 5px 0px',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const content = {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
padding: '20px 25px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paragraph = {
|
||||||
|
// fontSize: '12px',
|
||||||
|
lineHeight: '24px',
|
||||||
|
margin: '10px 0',
|
||||||
|
color: '#000000',
|
||||||
|
// fontFamily: 'Arial, Helvetica, sans-serif',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imageContainer = {
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
margin: '20px 0',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const profileImage = {
|
||||||
|
// border: '1px solid #ec489a',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const button = {
|
||||||
|
backgroundColor: '#4887ec',
|
||||||
|
borderRadius: '12px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
textDecoration: 'none',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 10px',
|
||||||
|
margin: '10px 0',
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "react-email-starter",
|
"name": "react-email-starter",
|
||||||
"version": "0.1.9",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "email dev",
|
"dev": "email dev --port 3001",
|
||||||
"build": "tsc -b"
|
"build": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/components": "0.0.33",
|
"@react-email/components": "0.0.33",
|
||||||
"react": "19.0.0",
|
"react-icons": "5.5.0",
|
||||||
"react-dom": "19.0.0",
|
"resend": "4.1.2",
|
||||||
"react-email": "3.0.7",
|
"react": "18.2.0",
|
||||||
"resend": "4.1.2"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/html-to-text": "9.0.4",
|
"@types/html-to-text": "9.0.4",
|
||||||
"@types/prismjs": "1.26.5",
|
"@types/prismjs": "1.26.5",
|
||||||
"@types/react": "19.0.10",
|
"@types/react": "18.3.5",
|
||||||
"@types/react-dom": "19.0.4"
|
"@types/react-dom": "18.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ import { chunk } from 'lodash'
|
|||||||
runScript(async ({ pg }) => {
|
runScript(async ({ pg }) => {
|
||||||
const directClient = createSupabaseDirectClient()
|
const directClient = createSupabaseDirectClient()
|
||||||
|
|
||||||
// Get all users and their corresponding lovers
|
// Get all users and their corresponding profiles
|
||||||
const users = await directClient.manyOrNone(`
|
const users = await directClient.manyOrNone(`
|
||||||
select u.id, u.data, l.twitter
|
select u.id, u.data, l.twitter
|
||||||
from users u
|
from users u
|
||||||
left join lovers l on l.user_id = u.id
|
left join profiles l on l.user_id = u.id
|
||||||
`)
|
`)
|
||||||
|
|
||||||
log('Found', users.length, 'users to migrate')
|
log('Found', users.length, 'users to migrate')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
select,
|
select,
|
||||||
from,
|
from,
|
||||||
where,
|
where,
|
||||||
} from '../shared/src/supabase/sql-builder'
|
} from 'shared/supabase/sql-builder'
|
||||||
import { SupabaseDirectClient } from 'shared/supabase/init'
|
import { SupabaseDirectClient } from 'shared/supabase/init'
|
||||||
|
|
||||||
runScript(async ({ pg }) => {
|
runScript(async ({ pg }) => {
|
||||||
@@ -27,7 +27,7 @@ const getNodes = async (pg: SupabaseDirectClient, nodeName: string) => {
|
|||||||
console.log(`\nSearching comments for ${nodeName}...`)
|
console.log(`\nSearching comments for ${nodeName}...`)
|
||||||
const commentQuery = renderSql(
|
const commentQuery = renderSql(
|
||||||
select('id, user_id, on_user_id, content'),
|
select('id, user_id, on_user_id, content'),
|
||||||
from('lover_comments'),
|
from('profile_comments'),
|
||||||
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeName}")')`)
|
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeName}")')`)
|
||||||
)
|
)
|
||||||
const comments = await pg.manyOrNone(commentQuery)
|
const comments = await pg.manyOrNone(commentQuery)
|
||||||
@@ -59,7 +59,7 @@ const getNodes = async (pg: SupabaseDirectClient, nodeName: string) => {
|
|||||||
console.log(`\nSearching profiles for ${nodeName}...`)
|
console.log(`\nSearching profiles for ${nodeName}...`)
|
||||||
const users = renderSql(
|
const users = renderSql(
|
||||||
select('user_id, bio'),
|
select('user_id, bio'),
|
||||||
from('lovers'),
|
from('profiles'),
|
||||||
where(`jsonb_path_exists(bio::jsonb, '$.**.type ? (@ == "${nodeName}")')`)
|
where(`jsonb_path_exists(bio::jsonb, '$.**.type ? (@ == "${nodeName}")')`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
select,
|
select,
|
||||||
from,
|
from,
|
||||||
where,
|
where,
|
||||||
} from '../shared/src/supabase/sql-builder'
|
} from 'shared/supabase/sql-builder'
|
||||||
import { type JSONContent } from '@tiptap/core'
|
import { type JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
const removeNodesOfType = (
|
const removeNodesOfType = (
|
||||||
@@ -33,7 +33,7 @@ runScript(async ({ pg }) => {
|
|||||||
console.log('\nSearching comments for linkPreviews...')
|
console.log('\nSearching comments for linkPreviews...')
|
||||||
const commentQuery = renderSql(
|
const commentQuery = renderSql(
|
||||||
select('id, content'),
|
select('id, content'),
|
||||||
from('lover_comments'),
|
from('profile_comments'),
|
||||||
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeType}")')`)
|
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeType}")')`)
|
||||||
)
|
)
|
||||||
const comments = await pg.manyOrNone(commentQuery)
|
const comments = await pg.manyOrNone(commentQuery)
|
||||||
@@ -45,7 +45,7 @@ runScript(async ({ pg }) => {
|
|||||||
console.log('before', comment.content)
|
console.log('before', comment.content)
|
||||||
console.log('after', newContent)
|
console.log('after', newContent)
|
||||||
|
|
||||||
await pg.none('update lover_comments set content = $1 where id = $2', [
|
await pg.none('update profile_comments set content = $1 where id = $2', [
|
||||||
newContent,
|
newContent,
|
||||||
comment.id,
|
comment.id,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"@tiptap/html": "2.3.2",
|
"@tiptap/html": "2.3.2",
|
||||||
"colors": "1.4.0",
|
"colors": "1.4.0",
|
||||||
"dayjs": "1.11.4",
|
"dayjs": "1.11.4",
|
||||||
"firebase-admin": "11.11.1",
|
"firebase-admin": "13.5.0",
|
||||||
"gcp-metadata": "6.1.0",
|
"gcp-metadata": "6.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"pg-promise": "11.4.1",
|
"pg-promise": "11.4.1",
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import { createSupabaseDirectClient } from './supabase/init'
|
|||||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
||||||
import { Notification } from 'common/notifications'
|
import { Notification } from 'common/notifications'
|
||||||
import { insertNotificationToSupabase } from './supabase/notifications'
|
import { insertNotificationToSupabase } from './supabase/notifications'
|
||||||
import { getLover } from './love/supabase'
|
import { getProfile } from './love/supabase'
|
||||||
|
|
||||||
export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
||||||
const { creator_id, target_id, like_id } = like
|
const { creator_id, target_id, like_id } = like
|
||||||
|
|
||||||
const targetPrivateUser = await getPrivateUser(target_id)
|
const targetPrivateUser = await getPrivateUser(target_id)
|
||||||
const lover = await getLover(creator_id)
|
const profile = await getProfile(creator_id)
|
||||||
|
|
||||||
if (!targetPrivateUser || !lover) return
|
if (!targetPrivateUser || !profile) return
|
||||||
|
|
||||||
const { sendToBrowser } = getNotificationDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
targetPrivateUser,
|
targetPrivateUser,
|
||||||
@@ -30,9 +30,9 @@ export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
|||||||
sourceId: like_id,
|
sourceId: like_id,
|
||||||
sourceType: 'love_like',
|
sourceType: 'love_like',
|
||||||
sourceUpdateType: 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUserName: lover.user.name,
|
sourceUserName: profile.user.name,
|
||||||
sourceUserUsername: lover.user.username,
|
sourceUserUsername: profile.user.username,
|
||||||
sourceUserAvatarUrl: lover.pinned_url ?? lover.user.avatarUrl,
|
sourceUserAvatarUrl: profile.pinned_url ?? profile.user.avatarUrl,
|
||||||
sourceText: '',
|
sourceText: '',
|
||||||
}
|
}
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
@@ -48,13 +48,13 @@ export const createLoveShipNotification = async (
|
|||||||
|
|
||||||
const creator = await getUser(creator_id)
|
const creator = await getUser(creator_id)
|
||||||
const targetPrivateUser = await getPrivateUser(recipientId)
|
const targetPrivateUser = await getPrivateUser(recipientId)
|
||||||
const lover = await getLover(otherTargetId)
|
const profile = await getProfile(otherTargetId)
|
||||||
|
|
||||||
if (!creator || !targetPrivateUser || !lover) {
|
if (!creator || !targetPrivateUser || !profile) {
|
||||||
console.error('Could not load user object', {
|
console.error('Could not load user object', {
|
||||||
creator,
|
creator,
|
||||||
targetPrivateUser,
|
targetPrivateUser,
|
||||||
lover,
|
profile,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -75,9 +75,9 @@ export const createLoveShipNotification = async (
|
|||||||
sourceId: ship_id,
|
sourceId: ship_id,
|
||||||
sourceType: 'love_ship',
|
sourceType: 'love_ship',
|
||||||
sourceUpdateType: 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUserName: lover.user.name,
|
sourceUserName: profile.user.name,
|
||||||
sourceUserUsername: lover.user.username,
|
sourceUserUsername: profile.user.username,
|
||||||
sourceUserAvatarUrl: lover.pinned_url ?? lover.user.avatarUrl,
|
sourceUserAvatarUrl: profile.pinned_url ?? profile.user.avatarUrl,
|
||||||
sourceText: '',
|
sourceText: '',
|
||||||
data: {
|
data: {
|
||||||
creatorId: creator_id,
|
creatorId: creator_id,
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { areGenderCompatible } from 'common/love/compatibility-util'
|
import { areGenderCompatible } from 'common/love/compatibility-util'
|
||||||
import { type Lover, type LoverRow } from 'common/love/lover'
|
import { type Profile, type ProfileRow } from 'common/love/profile'
|
||||||
import { type User } from 'common/user'
|
import { type User } from 'common/user'
|
||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
|
|
||||||
export type LoverAndUserRow = LoverRow & {
|
export type ProfileAndUserRow = ProfileRow & {
|
||||||
name: string
|
name: string
|
||||||
username: string
|
username: string
|
||||||
user: any
|
user: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertRow(row: LoverAndUserRow): Lover
|
export function convertRow(row: ProfileAndUserRow): Profile
|
||||||
export function convertRow(row: LoverAndUserRow | undefined): Lover | null {
|
export function convertRow(row: ProfileAndUserRow | undefined): Profile | null {
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
user: { ...row.user, name: row.name, username: row.username } as User,
|
user: { ...row.user, name: row.name, username: row.username } as User,
|
||||||
} as Lover
|
} as Profile
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOVER_COLS = 'lovers.*, name, username, users.data as user'
|
const LOVER_COLS = 'profiles.*, name, username, users.data as user'
|
||||||
|
|
||||||
export const getLover = async (userId: string) => {
|
export const getProfile = async (userId: string) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
return await pg.oneOrNone(
|
return await pg.oneOrNone(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${LOVER_COLS}
|
||||||
from
|
from
|
||||||
lovers
|
profiles
|
||||||
join
|
join
|
||||||
users on users.id = lovers.user_id
|
users on users.id = profiles.user_id
|
||||||
where
|
where
|
||||||
user_id = $1
|
user_id = $1
|
||||||
`,
|
`,
|
||||||
@@ -40,16 +40,16 @@ export const getLover = async (userId: string) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLovers = async (userIds: string[]) => {
|
export const getProfiles = async (userIds: string[]) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
return await pg.map(
|
return await pg.map(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${LOVER_COLS}
|
||||||
from
|
from
|
||||||
lovers
|
profiles
|
||||||
join
|
join
|
||||||
users on users.id = lovers.user_id
|
users on users.id = profiles.user_id
|
||||||
where
|
where
|
||||||
user_id = any($1)
|
user_id = any($1)
|
||||||
`,
|
`,
|
||||||
@@ -58,30 +58,30 @@ export const getLovers = async (userIds: string[]) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGenderCompatibleLovers = async (lover: LoverRow) => {
|
export const getGenderCompatibleProfiles = async (profile: ProfileRow) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const lovers = await pg.map(
|
const profiles = await pg.map(
|
||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${LOVER_COLS}
|
||||||
from lovers
|
from profiles
|
||||||
join
|
join
|
||||||
users on users.id = lovers.user_id
|
users on users.id = profiles.user_id
|
||||||
where
|
where
|
||||||
user_id != $(user_id)
|
user_id != $(user_id)
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)
|
||||||
and (data->>'userDeleted' != 'true' or data->>'userDeleted' is null)
|
and (data->>'userDeleted' != 'true' or data->>'userDeleted' is null)
|
||||||
and lovers.pinned_url is not null
|
and profiles.pinned_url is not null
|
||||||
`,
|
`,
|
||||||
{ ...lover },
|
{ ...profile },
|
||||||
convertRow
|
convertRow
|
||||||
)
|
)
|
||||||
return lovers.filter((l: Lover) => areGenderCompatible(lover, l))
|
return profiles.filter((l: Profile) => areGenderCompatible(profile, l))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCompatibleLovers = async (
|
export const getCompatibleProfiles = async (
|
||||||
lover: LoverRow,
|
profile: ProfileRow,
|
||||||
radiusKm: number | undefined
|
radiusKm: number | undefined
|
||||||
) => {
|
) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
@@ -89,9 +89,9 @@ export const getCompatibleLovers = async (
|
|||||||
`
|
`
|
||||||
select
|
select
|
||||||
${LOVER_COLS}
|
${LOVER_COLS}
|
||||||
from lovers
|
from profiles
|
||||||
join
|
join
|
||||||
users on users.id = lovers.user_id
|
users on users.id = profiles.user_id
|
||||||
where
|
where
|
||||||
user_id != $(user_id)
|
user_id != $(user_id)
|
||||||
and looking_for_matches
|
and looking_for_matches
|
||||||
@@ -99,19 +99,19 @@ export const getCompatibleLovers = async (
|
|||||||
and (data->>'userDeleted' != 'true' or data->>'userDeleted' is null)
|
and (data->>'userDeleted' != 'true' or data->>'userDeleted' is null)
|
||||||
|
|
||||||
-- Gender
|
-- Gender
|
||||||
and (lovers.gender = any($(pref_gender)) or lovers.gender = 'non-binary')
|
and (profiles.gender = any($(pref_gender)) or profiles.gender = 'non-binary')
|
||||||
and ($(gender) = any(lovers.pref_gender) or $(gender) = 'non-binary')
|
and ($(gender) = any(profiles.pref_gender) or $(gender) = 'non-binary')
|
||||||
|
|
||||||
-- Age
|
-- Age
|
||||||
and lovers.age >= $(pref_age_min)
|
and profiles.age >= $(pref_age_min)
|
||||||
and lovers.age <= $(pref_age_max)
|
and profiles.age <= $(pref_age_max)
|
||||||
and $(age) >= lovers.pref_age_min
|
and $(age) >= profiles.pref_age_min
|
||||||
and $(age) <= lovers.pref_age_max
|
and $(age) <= profiles.pref_age_max
|
||||||
|
|
||||||
-- Location
|
-- Location
|
||||||
and calculate_earth_distance_km($(city_latitude), $(city_longitude), lovers.city_latitude, lovers.city_longitude) < $(radiusKm)
|
and calculate_earth_distance_km($(city_latitude), $(city_longitude), profiles.city_latitude, profiles.city_longitude) < $(radiusKm)
|
||||||
`,
|
`,
|
||||||
{ ...lover, radiusKm: radiusKm ?? 40_000 },
|
{ ...profile, radiusKm: radiusKm ?? 40_000 },
|
||||||
convertRow
|
convertRow
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const newClient = (
|
|||||||
...settings,
|
...settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(config)
|
// console.log(config)
|
||||||
|
|
||||||
return pgp(config)
|
return pgp(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import {
|
import {createSupabaseDirectClient, SupabaseDirectClient,} from 'shared/supabase/init'
|
||||||
createSupabaseDirectClient,
|
|
||||||
SupabaseDirectClient,
|
|
||||||
} from 'shared/supabase/init'
|
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { convertPrivateUser, convertUser } from 'common/supabase/users'
|
import {convertPrivateUser, convertUser} from 'common/supabase/users'
|
||||||
import { log, type Logger } from 'shared/monitoring/log'
|
import {log, type Logger} from 'shared/monitoring/log'
|
||||||
import { metrics } from 'shared/monitoring/metrics'
|
import {metrics} from 'shared/monitoring/metrics'
|
||||||
|
|
||||||
export { metrics }
|
export { metrics }
|
||||||
export { log, type Logger }
|
export { log, type Logger }
|
||||||
@@ -62,8 +59,7 @@ export const isProd = () => {
|
|||||||
if (process.env.ENVIRONMENT) {
|
if (process.env.ENVIRONMENT) {
|
||||||
return process.env.ENVIRONMENT == 'PROD'
|
return process.env.ENVIRONMENT == 'PROD'
|
||||||
} else {
|
} else {
|
||||||
return admin.app().options.projectId === 'polylove'
|
return admin.app().options.projectId === 'compass-130ba'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LOCAL_DEV = process.env.GOOGLE_CLOUD_PROJECT == null
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Server as HttpServer } from 'node:http'
|
import { Server as HttpServer } from 'node:http'
|
||||||
import { Server as WebSocketServer, RawData, WebSocket } from 'ws'
|
import { Server as WebSocketServer, RawData, WebSocket } from 'ws'
|
||||||
import { isError } from 'lodash'
|
import { isError } from 'lodash'
|
||||||
import { LOCAL_DEV, log, metrics } from 'shared/utils'
|
import { log, metrics } from 'shared/utils'
|
||||||
import { Switchboard } from './switchboard'
|
import { Switchboard } from './switchboard'
|
||||||
import {
|
import {
|
||||||
BroadcastPayload,
|
BroadcastPayload,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ServerMessage,
|
ServerMessage,
|
||||||
CLIENT_MESSAGE_SCHEMA,
|
CLIENT_MESSAGE_SCHEMA,
|
||||||
} from 'common/api/websockets'
|
} from 'common/api/websockets'
|
||||||
|
import {LOCAL_DEV} from "common/envs/constants";
|
||||||
|
|
||||||
const SWITCHBOARD = new Switchboard()
|
const SWITCHBOARD = new Switchboard()
|
||||||
|
|
||||||
|
|||||||
40
backend/supabase/bookmarked_searches.sql
Normal file
40
backend/supabase/bookmarked_searches.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS bookmarked_searches (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
search_filters JSONB,
|
||||||
|
location JSONB,
|
||||||
|
last_notified_at TIMESTAMPTZ DEFAULT NULL,
|
||||||
|
search_name TEXT DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE bookmarked_searches ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON bookmarked_searches;
|
||||||
|
CREATE POLICY "public read" ON bookmarked_searches
|
||||||
|
FOR SELECT USING (true);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self delete" ON bookmarked_searches;
|
||||||
|
CREATE POLICY "self delete" ON bookmarked_searches
|
||||||
|
FOR DELETE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self insert" ON bookmarked_searches;
|
||||||
|
CREATE POLICY "self insert" ON bookmarked_searches
|
||||||
|
FOR INSERT WITH CHECK (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self update" ON bookmarked_searches;
|
||||||
|
CREATE POLICY "self update" ON bookmarked_searches
|
||||||
|
FOR UPDATE USING (creator_id = firebase_uid());
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS bookmarked_searches_creator_id_created_time_idx
|
||||||
|
ON public.bookmarked_searches (creator_id, created_time DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS bookmarked_searches_creator_id_idx
|
||||||
|
ON public.bookmarked_searches (creator_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS bookmarked_searches_search_name_idx
|
||||||
|
ON public.bookmarked_searches (search_name);
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.to_jsonb (jsonb) returns jsonb language sql immutable parallel SAFE strict as $function$ select $1 $function$;
|
or replace function public.to_jsonb (jsonb) returns jsonb language sql immutable parallel SAFE strict as $function$ select $1 $function$;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ END;
|
|||||||
$function$;
|
$function$;
|
||||||
|
|
||||||
create
|
create
|
||||||
or replace function public.get_love_question_answers_and_lovers (p_question_id bigint) returns setof record language plpgsql as $function$
|
or replace function public.get_love_question_answers_and_profiles (p_question_id bigint) returns setof record language plpgsql as $function$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT
|
SELECT
|
||||||
@@ -29,16 +29,16 @@ SELECT
|
|||||||
love_answers.free_response,
|
love_answers.free_response,
|
||||||
love_answers.multiple_choice,
|
love_answers.multiple_choice,
|
||||||
love_answers.integer,
|
love_answers.integer,
|
||||||
lovers.age,
|
profiles.age,
|
||||||
lovers.gender,
|
profiles.gender,
|
||||||
lovers.city,
|
profiles.city,
|
||||||
users.data
|
users.data
|
||||||
FROM
|
FROM
|
||||||
lovers
|
profiles
|
||||||
JOIN
|
JOIN
|
||||||
love_answers ON lovers.user_id = love_answers.creator_id
|
love_answers ON profiles.user_id = love_answers.creator_id
|
||||||
join
|
join
|
||||||
users on lovers.user_id = users.id
|
users on profiles.user_id = users.id
|
||||||
WHERE
|
WHERE
|
||||||
love_answers.question_id = p_question_id
|
love_answers.question_id = p_question_id
|
||||||
order by love_answers.created_time desc;
|
order by love_answers.created_time desc;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_answers (
|
CREATE TABLE IF NOT EXISTS love_answers (
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_compatibility_answers (
|
CREATE TABLE IF NOT EXISTS love_compatibility_answers (
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_likes (
|
CREATE TABLE IF NOT EXISTS love_likes (
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
creator_id TEXT NOT NULL,
|
creator_id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_questions (
|
CREATE TABLE IF NOT EXISTS love_questions (
|
||||||
answer_type TEXT DEFAULT 'free_response' NOT NULL,
|
answer_type TEXT DEFAULT 'free_response' NOT NULL,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_ships (
|
CREATE TABLE IF NOT EXISTS love_ships (
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
creator_id TEXT NOT NULL,
|
creator_id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_stars (
|
CREATE TABLE IF NOT EXISTS love_stars (
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
creator_id TEXT NOT NULL,
|
creator_id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS love_waitlist (
|
CREATE TABLE IF NOT EXISTS love_waitlist (
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'lover_visibility') THEN
|
|
||||||
CREATE TYPE lover_visibility AS ENUM ('public', 'member');
|
|
||||||
END IF;
|
|
||||||
END$$;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS lovers (
|
|
||||||
age INTEGER DEFAULT 18 NOT NULL,
|
|
||||||
bio JSON,
|
|
||||||
born_in_location TEXT,
|
|
||||||
city TEXT NOT NULL,
|
|
||||||
city_latitude NUMERIC(9, 6),
|
|
||||||
city_longitude NUMERIC(9, 6),
|
|
||||||
comments_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
|
||||||
company TEXT,
|
|
||||||
country TEXT,
|
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
drinks_per_month INTEGER,
|
|
||||||
education_level TEXT,
|
|
||||||
ethnicity TEXT[],
|
|
||||||
gender TEXT NOT NULL,
|
|
||||||
geodb_city_id TEXT,
|
|
||||||
has_kids INTEGER,
|
|
||||||
height_in_inches INTEGER,
|
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
|
||||||
is_smoker BOOLEAN,
|
|
||||||
is_vegetarian_or_vegan BOOLEAN,
|
|
||||||
last_online_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
|
||||||
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
|
||||||
messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL,
|
|
||||||
occupation TEXT,
|
|
||||||
occupation_title TEXT,
|
|
||||||
photo_urls TEXT[],
|
|
||||||
pinned_url TEXT,
|
|
||||||
political_beliefs TEXT[],
|
|
||||||
pref_age_max INTEGER DEFAULT 100 NOT NULL,
|
|
||||||
pref_age_min INTEGER DEFAULT 18 NOT NULL,
|
|
||||||
pref_gender TEXT[] NOT NULL,
|
|
||||||
pref_relation_styles TEXT[] NOT NULL,
|
|
||||||
referred_by_username TEXT,
|
|
||||||
region_code TEXT,
|
|
||||||
religious_belief_strength INTEGER,
|
|
||||||
religious_beliefs TEXT,
|
|
||||||
twitter TEXT,
|
|
||||||
university TEXT,
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
visibility lover_visibility DEFAULT 'member'::lover_visibility NOT NULL,
|
|
||||||
wants_kids_strength INTEGER DEFAULT 0 NOT NULL,
|
|
||||||
website TEXT,
|
|
||||||
CONSTRAINT lovers_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
ALTER TABLE lovers ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Policies
|
|
||||||
DROP POLICY IF EXISTS "public read" ON lovers;
|
|
||||||
|
|
||||||
CREATE POLICY "public read" ON lovers
|
|
||||||
FOR SELECT
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
DROP POLICY IF EXISTS "self update" ON lovers;
|
|
||||||
|
|
||||||
CREATE POLICY "self update" ON lovers
|
|
||||||
FOR UPDATE
|
|
||||||
WITH CHECK ((user_id = firebase_uid()));
|
|
||||||
|
|
||||||
-- Indexes
|
|
||||||
DROP INDEX IF EXISTS lovers_user_id_idx;
|
|
||||||
|
|
||||||
CREATE INDEX lovers_user_id_idx ON public.lovers USING btree (user_id);
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS unique_user_id;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX unique_user_id ON public.lovers USING btree (user_id);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
BEGIN;
|
BEGIN;
|
||||||
\i backend/supabase/functions.sql
|
\i backend/supabase/functions.sql
|
||||||
\i backend/supabase/firebase.sql
|
\i backend/supabase/firebase.sql
|
||||||
\i backend/supabase/lovers.sql
|
\i backend/supabase/profiles.sql
|
||||||
\i backend/supabase/users.sql
|
\i backend/supabase/users.sql
|
||||||
\i backend/supabase/private_user_message_channels.sql
|
\i backend/supabase/private_user_message_channels.sql
|
||||||
\i backend/supabase/private_user_message_channel_members.sql
|
\i backend/supabase/private_user_message_channel_members.sql
|
||||||
@@ -9,7 +9,7 @@ BEGIN;
|
|||||||
\i backend/supabase/private_user_messages.sql
|
\i backend/supabase/private_user_messages.sql
|
||||||
\i backend/supabase/private_user_seen_message_channels.sql
|
\i backend/supabase/private_user_seen_message_channels.sql
|
||||||
\i backend/supabase/love_answers.sql
|
\i backend/supabase/love_answers.sql
|
||||||
\i backend/supabase/lover_comments.sql
|
\i backend/supabase/profile_comments.sql
|
||||||
\i backend/supabase/love_compatibility_answers.sql
|
\i backend/supabase/love_compatibility_answers.sql
|
||||||
\i backend/supabase/love_likes.sql
|
\i backend/supabase/love_likes.sql
|
||||||
\i backend/supabase/love_questions.sql
|
\i backend/supabase/love_questions.sql
|
||||||
@@ -19,4 +19,6 @@ BEGIN;
|
|||||||
\i backend/supabase/user_events.sql
|
\i backend/supabase/user_events.sql
|
||||||
\i backend/supabase/user_notifications.sql
|
\i backend/supabase/user_notifications.sql
|
||||||
\i backend/supabase/functions_others.sql
|
\i backend/supabase/functions_others.sql
|
||||||
|
\i backend/supabase/reports.sql
|
||||||
|
\i backend/supabase/bookmarked_searches.sql
|
||||||
COMMIT;
|
COMMIT;
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
-- This file is copied from https://github.com/manifoldmarkets/manifold/blob/main/backend/supabase/reports.sql
|
|
||||||
create table if not exists
|
|
||||||
reports (
|
|
||||||
content_id text not null,
|
|
||||||
content_owner_id text not null,
|
|
||||||
content_type text not null,
|
|
||||||
created_time timestamp with time zone default now(),
|
|
||||||
description text,
|
|
||||||
id text default uuid_generate_v4 () not null,
|
|
||||||
parent_id text,
|
|
||||||
parent_type text,
|
|
||||||
user_id text not null
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Foreign Keys
|
|
||||||
alter table reports
|
|
||||||
add constraint reports_content_owner_id_fkey foreign key (content_owner_id) references users (id);
|
|
||||||
|
|
||||||
alter table reports
|
|
||||||
add constraint reports_user_id_fkey foreign key (user_id) references users (id);
|
|
||||||
|
|
||||||
-- Row Level Security
|
|
||||||
alter table reports enable row level security;
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS private_user_message_channel_members (
|
CREATE TABLE IF NOT EXISTS private_user_message_channel_members (
|
||||||
channel_id BIGINT NOT NULL,
|
channel_id BIGINT NOT NULL,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS private_user_message_channels (
|
CREATE TABLE IF NOT EXISTS private_user_message_channels (
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS private_user_messages (
|
CREATE TABLE IF NOT EXISTS private_user_messages (
|
||||||
channel_id BIGINT NOT NULL,
|
channel_id BIGINT NOT NULL,
|
||||||
content JSONB NOT NULL,
|
content JSONB NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS private_user_seen_message_channels (
|
CREATE TABLE IF NOT EXISTS private_user_seen_message_channels (
|
||||||
channel_id BIGINT NOT NULL,
|
channel_id BIGINT NOT NULL,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS private_users (
|
CREATE TABLE IF NOT EXISTS private_users (
|
||||||
data JSONB NOT NULL,
|
data JSONB NOT NULL,
|
||||||
id TEXT NOT NULL,
|
id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
CREATE TABLE IF NOT EXISTS profile_comments (
|
||||||
CREATE TABLE IF NOT EXISTS lover_comments (
|
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
content JSONB NOT NULL,
|
content JSONB NOT NULL,
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
@@ -13,12 +12,12 @@ CREATE TABLE IF NOT EXISTS lover_comments (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Row Level Security
|
-- Row Level Security
|
||||||
ALTER TABLE lover_comments ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE profile_comments ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- Policies
|
-- Policies
|
||||||
DROP POLICY IF EXISTS "public read" ON lover_comments;
|
DROP POLICY IF EXISTS "public read" ON profile_comments;
|
||||||
CREATE POLICY "public read" ON lover_comments FOR ALL USING (true);
|
CREATE POLICY "public read" ON profile_comments FOR ALL USING (true);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
CREATE INDEX IF NOT EXISTS lover_comments_user_id_idx
|
CREATE INDEX IF NOT EXISTS profile_comments_user_id_idx
|
||||||
ON public.lover_comments USING btree (on_user_id);
|
ON public.profile_comments USING btree (on_user_id);
|
||||||
134
backend/supabase/profiles.sql
Normal file
134
backend/supabase/profiles.sql
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'profile_visibility') THEN
|
||||||
|
CREATE TYPE profile_visibility AS ENUM ('public', 'member');
|
||||||
|
END IF;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS profiles (
|
||||||
|
age INTEGER NULL,
|
||||||
|
bio JSONB,
|
||||||
|
born_in_location TEXT,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
city_latitude NUMERIC(9, 6),
|
||||||
|
city_longitude NUMERIC(9, 6),
|
||||||
|
comments_enabled BOOLEAN DEFAULT TRUE NOT NULL,
|
||||||
|
company TEXT,
|
||||||
|
country TEXT,
|
||||||
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
drinks_per_month INTEGER,
|
||||||
|
education_level TEXT,
|
||||||
|
ethnicity TEXT[],
|
||||||
|
gender TEXT NOT NULL,
|
||||||
|
geodb_city_id TEXT,
|
||||||
|
has_kids INTEGER,
|
||||||
|
height_in_inches INTEGER,
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||||
|
is_smoker BOOLEAN,
|
||||||
|
is_vegetarian_or_vegan BOOLEAN,
|
||||||
|
last_online_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
last_modification_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
|
looking_for_matches BOOLEAN DEFAULT TRUE NOT NULL,
|
||||||
|
messaging_status TEXT DEFAULT 'open'::TEXT NOT NULL,
|
||||||
|
occupation TEXT,
|
||||||
|
occupation_title TEXT,
|
||||||
|
photo_urls TEXT[],
|
||||||
|
pinned_url TEXT,
|
||||||
|
political_beliefs TEXT[],
|
||||||
|
pref_age_max INTEGER NULL,
|
||||||
|
pref_age_min INTEGER NULL,
|
||||||
|
pref_gender TEXT[] NOT NULL,
|
||||||
|
pref_relation_styles TEXT[] NOT NULL,
|
||||||
|
referred_by_username TEXT,
|
||||||
|
region_code TEXT,
|
||||||
|
religious_belief_strength INTEGER,
|
||||||
|
religious_beliefs TEXT,
|
||||||
|
twitter TEXT,
|
||||||
|
university TEXT,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
||||||
|
wants_kids_strength INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
website TEXT,
|
||||||
|
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Policies
|
||||||
|
DROP POLICY IF EXISTS "public read" ON profiles;
|
||||||
|
|
||||||
|
CREATE POLICY "public read" ON profiles
|
||||||
|
FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "self update" ON profiles;
|
||||||
|
|
||||||
|
CREATE POLICY "self update" ON profiles
|
||||||
|
FOR UPDATE
|
||||||
|
WITH CHECK ((user_id = firebase_uid()));
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
DROP INDEX IF EXISTS profiles_user_id_idx;
|
||||||
|
CREATE INDEX profiles_user_id_idx ON public.profiles USING btree (user_id);
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS unique_user_id;
|
||||||
|
CREATE UNIQUE INDEX unique_user_id ON public.profiles USING btree (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_profiles_last_mod_24h
|
||||||
|
ON public.profiles USING btree (last_modification_time);
|
||||||
|
|
||||||
|
-- Functions and Triggers
|
||||||
|
CREATE
|
||||||
|
OR REPLACE FUNCTION update_last_modification_time()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.last_online_time IS DISTINCT FROM OLD.last_online_time AND row(NEW.*) = row(OLD.*) THEN
|
||||||
|
-- Only last_online_time changed, do nothing
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Some other column changed
|
||||||
|
NEW.last_modification_time = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$
|
||||||
|
LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_update_last_mod_time
|
||||||
|
BEFORE UPDATE
|
||||||
|
ON profiles
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_last_modification_time();
|
||||||
|
|
||||||
|
-- pg_trgm
|
||||||
|
create extension if not exists pg_trgm;
|
||||||
|
|
||||||
|
CREATE INDEX profiles_bio_trgm_idx
|
||||||
|
ON profiles USING gin ((bio::text) gin_trgm_ops);
|
||||||
|
|
||||||
|
|
||||||
|
--- bio_text
|
||||||
|
-- ALTER TABLE profiles ADD COLUMN bio_text tsvector;
|
||||||
|
--
|
||||||
|
-- CREATE OR REPLACE FUNCTION profiles_bio_tsvector_update()
|
||||||
|
-- RETURNS trigger AS $$
|
||||||
|
-- BEGIN
|
||||||
|
-- new.bio_text := to_tsvector(
|
||||||
|
-- 'english',
|
||||||
|
-- (
|
||||||
|
-- SELECT string_agg(trim(both '"' from x::text), ' ')
|
||||||
|
-- FROM jsonb_path_query(new.bio, '$.**.text'::jsonpath) AS x
|
||||||
|
-- )
|
||||||
|
-- );
|
||||||
|
-- RETURN new;
|
||||||
|
-- END;
|
||||||
|
-- $$ LANGUAGE plpgsql;
|
||||||
|
--
|
||||||
|
-- CREATE TRIGGER profiles_bio_tsvector_trigger
|
||||||
|
-- BEFORE INSERT OR UPDATE OF bio ON profiles
|
||||||
|
-- FOR EACH ROW EXECUTE FUNCTION profiles_bio_tsvector_update();
|
||||||
|
--
|
||||||
|
-- create index on profiles using gin(bio_text);
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
create table if not exists
|
create table if not exists
|
||||||
reports (
|
reports (
|
||||||
content_id text not null,
|
content_id text not null,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
create table if not exists
|
create table if not exists
|
||||||
temp_users (
|
temp_users (
|
||||||
created_time timestamp with time zone,
|
created_time timestamp with time zone,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS user_events (
|
CREATE TABLE IF NOT EXISTS user_events (
|
||||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
ad_id TEXT,
|
ad_id TEXT,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS user_notifications (
|
CREATE TABLE IF NOT EXISTS user_notifications (
|
||||||
notification_id TEXT NOT NULL,
|
notification_id TEXT NOT NULL,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||||
data JSONB NOT NULL,
|
data JSONB NOT NULL,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
-- This file is autogenerated from regen-schema.ts
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
contentSchema,
|
contentSchema,
|
||||||
combinedLoveUsersSchema,
|
combinedLoveUsersSchema,
|
||||||
baseLoversSchema,
|
baseProfilesSchema,
|
||||||
arraybeSchema,
|
arraybeSchema,
|
||||||
} from 'common/api/zod-types'
|
} from 'common/api/zod-types'
|
||||||
import { PrivateChatMessage } from 'common/chat-message'
|
import { PrivateChatMessage } from 'common/chat-message'
|
||||||
import { CompatibilityScore } from 'common/love/compatibility-score'
|
import { CompatibilityScore } from 'common/love/compatibility-score'
|
||||||
import { MAX_COMPATIBILITY_QUESTION_LENGTH } from 'common/love/constants'
|
import { MAX_COMPATIBILITY_QUESTION_LENGTH } from 'common/love/constants'
|
||||||
import { Lover, LoverRow } from 'common/love/lover'
|
import { Profile, ProfileRow } from 'common/love/profile'
|
||||||
import { Row } from 'common/supabase/utils'
|
import { Row } from 'common/supabase/utils'
|
||||||
import { PrivateUser, User } from 'common/user'
|
import { PrivateUser, User } from 'common/user'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -88,11 +88,11 @@ export const API = (_apiTypeCheck = {
|
|||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
},
|
},
|
||||||
'create-lover': {
|
'create-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
returns: {} as Row<'lovers'>,
|
returns: {} as Row<'profiles'>,
|
||||||
props: baseLoversSchema,
|
props: baseProfilesSchema,
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -143,11 +143,11 @@ export const API = (_apiTypeCheck = {
|
|||||||
}),
|
}),
|
||||||
returns: {} as FullUser,
|
returns: {} as FullUser,
|
||||||
},
|
},
|
||||||
'update-lover': {
|
'update-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
props: combinedLoveUsersSchema.partial(),
|
props: combinedLoveUsersSchema.partial(),
|
||||||
returns: {} as LoverRow,
|
returns: {} as ProfileRow,
|
||||||
},
|
},
|
||||||
'update-notif-settings': {
|
'update-notif-settings': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -212,14 +212,14 @@ export const API = (_apiTypeCheck = {
|
|||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
},
|
},
|
||||||
'compatible-lovers': {
|
'compatible-profiles': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
authed: false,
|
authed: false,
|
||||||
props: z.object({ userId: z.string() }),
|
props: z.object({ userId: z.string() }),
|
||||||
returns: {} as {
|
returns: {} as {
|
||||||
lover: Lover
|
profile: Profile
|
||||||
compatibleLovers: Lover[]
|
compatibleProfiles: Profile[]
|
||||||
loverCompatibilityScores: {
|
profileCompatibilityScores: {
|
||||||
[userId: string]: CompatibilityScore
|
[userId: string]: CompatibilityScore
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -246,7 +246,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
})[]
|
})[]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'like-lover': {
|
'like-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
props: z.object({
|
props: z.object({
|
||||||
@@ -257,7 +257,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'ship-lovers': {
|
'ship-profiles': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
props: z.object({
|
props: z.object({
|
||||||
@@ -293,7 +293,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
hasFreeLike: boolean
|
hasFreeLike: boolean
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'star-lover': {
|
'star-profile': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
props: z.object({
|
props: z.object({
|
||||||
@@ -304,7 +304,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'get-lovers': {
|
'get-profiles': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
authed: false,
|
authed: false,
|
||||||
props: z
|
props: z
|
||||||
@@ -331,10 +331,10 @@ export const API = (_apiTypeCheck = {
|
|||||||
.strict(),
|
.strict(),
|
||||||
returns: {} as {
|
returns: {} as {
|
||||||
status: 'success' | 'fail'
|
status: 'success' | 'fail'
|
||||||
lovers: Lover[]
|
profiles: Profile[]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'get-lover-answers': {
|
'get-profile-answers': {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
authed: false,
|
authed: false,
|
||||||
props: z.object({ userId: z.string() }).strict(),
|
props: z.object({ userId: z.string() }).strict(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { type APIPath } from './schema'
|
|
||||||
|
|
||||||
type ErrorCode =
|
type ErrorCode =
|
||||||
| 400 // your input is bad (like zod is mad)
|
| 400 // your input is bad (like zod is mad)
|
||||||
@@ -20,8 +19,8 @@ export class APIError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pathWithPrefix(path: APIPath) {
|
export function pathWithPrefix(path: string) {
|
||||||
return `v0/${path}`
|
return `/v0${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebsocketUrl() {
|
export function getWebsocketUrl() {
|
||||||
|
|||||||
@@ -42,19 +42,18 @@ const genderType = z.string()
|
|||||||
// ])
|
// ])
|
||||||
const genderTypes = z.array(genderType)
|
const genderTypes = z.array(genderType)
|
||||||
|
|
||||||
export const baseLoversSchema = z.object({
|
export const baseProfilesSchema = z.object({
|
||||||
// Required fields
|
// Required fields
|
||||||
age: z.number().min(18).max(100),
|
age: z.number().min(18).max(100).optional(),
|
||||||
gender: genderType,
|
gender: genderType,
|
||||||
pref_gender: genderTypes,
|
pref_gender: genderTypes,
|
||||||
pref_age_min: z.number().min(18).max(999),
|
pref_age_min: z.number().min(18).max(100).optional(),
|
||||||
pref_age_max: z.number().min(18).max(1000),
|
pref_age_max: z.number().min(18).max(100).optional(),
|
||||||
pref_relation_styles: z.array(
|
pref_relation_styles: z.array(
|
||||||
z.union([
|
z.union([
|
||||||
z.literal('mono'),
|
z.literal('collaboration'),
|
||||||
z.literal('poly'),
|
z.literal('friendship'),
|
||||||
z.literal('open'),
|
z.literal('relationship'),
|
||||||
z.literal('other'),
|
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
wants_kids_strength: z.number(),
|
wants_kids_strength: z.number(),
|
||||||
@@ -75,7 +74,7 @@ export const baseLoversSchema = z.object({
|
|||||||
referred_by_username: z.string().optional(),
|
referred_by_username: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const optionalLoversSchema = z.object({
|
const optionalProfilesSchema = z.object({
|
||||||
political_beliefs: z.array(z.string()).optional(),
|
political_beliefs: z.array(z.string()).optional(),
|
||||||
religious_belief_strength: z.number().optional(),
|
religious_belief_strength: z.number().optional(),
|
||||||
religious_beliefs: z.string().optional(),
|
religious_beliefs: z.string().optional(),
|
||||||
@@ -101,4 +100,4 @@ const optionalLoversSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const combinedLoveUsersSchema =
|
export const combinedLoveUsersSchema =
|
||||||
baseLoversSchema.merge(optionalLoversSchema)
|
baseProfilesSchema.merge(optionalProfilesSchema)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export type Comment = {
|
|||||||
replyToCommentId?: string
|
replyToCommentId?: string
|
||||||
userId: string
|
userId: string
|
||||||
|
|
||||||
// lover
|
// profile
|
||||||
commentType: 'lover'
|
commentType: 'profile'
|
||||||
onUserId: string
|
onUserId: string
|
||||||
|
|
||||||
/** @deprecated - content now stored as JSON in content*/
|
/** @deprecated - content now stored as JSON in content*/
|
||||||
|
|||||||
2
common/src/constants.ts
Normal file
2
common/src/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const MAX_INT = 99999
|
||||||
|
export const MIN_INT = -MAX_INT
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DEV_CONFIG } from './dev'
|
import {DEV_CONFIG} from './dev'
|
||||||
import { EnvConfig, PROD_CONFIG } from './prod'
|
import {EnvConfig, PROD_CONFIG} from './prod'
|
||||||
|
|
||||||
// Valid in web client & Vercel deployments only.
|
// Valid in web client & Vercel deployments only.
|
||||||
export const ENV = (process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD') as
|
export const ENV = (process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD') as
|
||||||
@@ -107,3 +107,6 @@ export const RESERVED_PATHS = [
|
|||||||
'web',
|
'web',
|
||||||
'welcome',
|
'welcome',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const LOCAL_WEB_URL = 'http://localhost:3000';
|
||||||
|
export const LOCAL_DEV = process.env.GOOGLE_CLOUD_PROJECT == null
|
||||||
@@ -2,4 +2,17 @@ import { EnvConfig, PROD_CONFIG } from './prod'
|
|||||||
|
|
||||||
export const DEV_CONFIG: EnvConfig = {
|
export const DEV_CONFIG: EnvConfig = {
|
||||||
...PROD_CONFIG,
|
...PROD_CONFIG,
|
||||||
|
supabaseInstanceId: 'zbspxezubpzxmuxciurg',
|
||||||
|
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpic3B4ZXp1YnB6eG11eGNpdXJnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTc2ODM0MTMsImV4cCI6MjA3MzI1OTQxM30.ZkM7zlawP8Nke0T3KJrqpOQ4DzqPaXTaJXLC2WU8Y7c',
|
||||||
|
firebaseConfig: {
|
||||||
|
apiKey: "AIzaSyBspL9glBXWbMsjmtt36dgb2yU0YGGhzKo",
|
||||||
|
authDomain: "compass-57c3c.firebaseapp.com",
|
||||||
|
projectId: "compass-57c3c",
|
||||||
|
storageBucket: "compass-57c3c.firebasestorage.app",
|
||||||
|
privateBucket: 'compass-private.firebasestorage.app',
|
||||||
|
messagingSenderId: "297460199314",
|
||||||
|
appId: "1:297460199314:web:c45678c54285910e255b4b",
|
||||||
|
measurementId: "G-N6LZ64EMJ2",
|
||||||
|
region: 'us-west1',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type FirebaseConfig = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PROD_CONFIG: EnvConfig = {
|
export const PROD_CONFIG: EnvConfig = {
|
||||||
posthogKey: 'phc_xT16KyBj7GsWnAwifoH4HiWKTFhuohRrfy3t5DK6ZIv',
|
posthogKey: 'phc_tFvQzHiMVdaAIgE38xqYomMN8q8SB5K45fqmkKNjfBU',
|
||||||
domain: 'compassmeet.com',
|
domain: 'compassmeet.com',
|
||||||
firebaseConfig: {
|
firebaseConfig: {
|
||||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '',
|
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '',
|
||||||
@@ -46,7 +46,7 @@ export const PROD_CONFIG: EnvConfig = {
|
|||||||
cloudRunId: 'w3txbmd3ba',
|
cloudRunId: 'w3txbmd3ba',
|
||||||
cloudRunRegion: 'uc',
|
cloudRunRegion: 'uc',
|
||||||
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
|
supabaseInstanceId: 'ltzepxnhhnrnvovqblfr',
|
||||||
supabaseAnonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx0emVweG5oaG5ybnZvdnFibGZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU5NjczNjgsImV4cCI6MjA3MTU0MzM2OH0.pbazcrVOG7Kh_IgblRu2VAfoBe3-xheNfRzAto7xvzY',
|
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_KEY || '',
|
||||||
apiEndpoint: 'api.compassmeet.com',
|
apiEndpoint: 'api.compassmeet.com',
|
||||||
adminIds: [
|
adminIds: [
|
||||||
'0vaZsIJk9zLVOWY4gb61gTrRIU73', // Martin
|
'0vaZsIJk9zLVOWY4gb61gTrRIU73', // Martin
|
||||||
|
|||||||
52
common/src/filters.ts
Normal file
52
common/src/filters.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {Profile, ProfileRow} from "common/love/profile";
|
||||||
|
import {cloneDeep} from "lodash";
|
||||||
|
import {filterDefined} from "common/util/array";
|
||||||
|
|
||||||
|
export type FilterFields = {
|
||||||
|
orderBy: 'last_online_time' | 'created_time' | 'compatibility_score'
|
||||||
|
geodbCityIds: string[] | null
|
||||||
|
genders: string[]
|
||||||
|
name: string | undefined
|
||||||
|
} & Pick<
|
||||||
|
ProfileRow,
|
||||||
|
| 'wants_kids_strength'
|
||||||
|
| 'pref_relation_styles'
|
||||||
|
| 'is_smoker'
|
||||||
|
| 'has_kids'
|
||||||
|
| 'pref_gender'
|
||||||
|
| 'pref_age_min'
|
||||||
|
| 'pref_age_max'
|
||||||
|
>
|
||||||
|
export const orderProfiles = (
|
||||||
|
profiles: Profile[],
|
||||||
|
starredUserIds: string[] | undefined
|
||||||
|
) => {
|
||||||
|
if (!profiles) return
|
||||||
|
|
||||||
|
let s = cloneDeep(profiles)
|
||||||
|
|
||||||
|
if (starredUserIds) {
|
||||||
|
s = filterDefined([
|
||||||
|
...starredUserIds.map((id) => s.find((l) => l.user_id === id)),
|
||||||
|
...s.filter((l) => !starredUserIds.includes(l.user_id)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// s = alternateWomenAndMen(s)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
export const initialFilters: Partial<FilterFields> = {
|
||||||
|
geodbCityIds: undefined,
|
||||||
|
name: undefined,
|
||||||
|
genders: undefined,
|
||||||
|
pref_age_max: undefined,
|
||||||
|
pref_age_min: undefined,
|
||||||
|
has_kids: undefined,
|
||||||
|
wants_kids_strength: undefined,
|
||||||
|
is_smoker: undefined,
|
||||||
|
pref_relation_styles: undefined,
|
||||||
|
pref_gender: undefined,
|
||||||
|
orderBy: 'created_time',
|
||||||
|
}
|
||||||
|
export type OriginLocation = { id: string; name: string }
|
||||||
32
common/src/geodb.ts
Normal file
32
common/src/geodb.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
export const geodbHost = 'wft-geo-db.p.rapidapi.com'
|
||||||
|
|
||||||
|
export const geodbFetch = async (endpoint: string) => {
|
||||||
|
const apiKey = process.env.GEODB_API_KEY
|
||||||
|
if (!apiKey) {
|
||||||
|
return {status: 'failure', data: 'Missing GEODB API key'}
|
||||||
|
}
|
||||||
|
const baseUrl = `https://${geodbHost}/v1/geo`
|
||||||
|
const url = `${baseUrl}${endpoint}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-RapidAPI-Key': apiKey,
|
||||||
|
'X-RapidAPI-Host': geodbHost,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${res.status} ${await res.text()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
console.log('geodbFetch', endpoint, data)
|
||||||
|
return {status: 'success', data}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('geodbFetch', endpoint, error)
|
||||||
|
return {status: 'failure', data: error}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
common/src/has-kids.ts
Normal file
51
common/src/has-kids.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export interface HasKidLabel {
|
||||||
|
name: string
|
||||||
|
shortName: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HasKidsLabelsMap {
|
||||||
|
[key: string]: HasKidLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasKidsLabels: HasKidsLabelsMap = {
|
||||||
|
no_preference: {
|
||||||
|
name: 'Any kids',
|
||||||
|
shortName: 'Any kids',
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
has_kids: {
|
||||||
|
name: 'Has kids',
|
||||||
|
shortName: 'Yes',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
doesnt_have_kids: {
|
||||||
|
name: `Doesn't have kids`,
|
||||||
|
shortName: 'No',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const hasKidsNames = Object.values(hasKidsLabels).reduce<Record<number, string>>(
|
||||||
|
(acc, {value, name}) => {
|
||||||
|
acc[value] = name
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const generateChoicesMap = (
|
||||||
|
labels: HasKidsLabelsMap
|
||||||
|
): Record<string, number> => {
|
||||||
|
return Object.values(labels).reduce(
|
||||||
|
(acc: Record<string, number>, label: HasKidLabel) => {
|
||||||
|
acc[label.shortName] = label.value
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const NO_PREFERENCE_STRENGTH = -1
|
||||||
|
// export const WANTS_KIDS_STRENGTH = 2
|
||||||
|
// export const DOESNT_WANT_KIDS_STRENGTH = 0
|
||||||
26
common/src/love/bookmarked_searches.ts
Normal file
26
common/src/love/bookmarked_searches.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export interface MatchPrivateUser {
|
||||||
|
email: string
|
||||||
|
notificationPreferences: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchUser {
|
||||||
|
name: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchesType {
|
||||||
|
description: {
|
||||||
|
filters: any; // You might want to replace 'any' with a more specific type
|
||||||
|
location: any; // You might want to replace 'any' with a more specific type
|
||||||
|
};
|
||||||
|
matches: MatchUser[]; // You might want to replace 'any' with a more specific type
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchesByUserType {
|
||||||
|
[key: string]: {
|
||||||
|
user: any;
|
||||||
|
privateUser: any;
|
||||||
|
matches: MatchesType[];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { keyBy, sumBy } from 'lodash'
|
import { keyBy, sumBy } from 'lodash'
|
||||||
import { LoverRow } from 'common/love/lover'
|
import { ProfileRow } from 'common/love/profile'
|
||||||
import { Row as rowFor } from 'common/supabase/utils'
|
import { Row as rowFor } from 'common/supabase/utils'
|
||||||
import {
|
import {
|
||||||
areAgeCompatible,
|
areAgeCompatible,
|
||||||
@@ -132,14 +132,14 @@ export function getScoredAnswerCompatibility(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getLoversCompatibilityFactor = (
|
export const getProfilesCompatibilityFactor = (
|
||||||
lover1: LoverRow,
|
profile1: ProfileRow,
|
||||||
lover2: LoverRow
|
profile2: ProfileRow
|
||||||
) => {
|
) => {
|
||||||
let multiplier = 1
|
let multiplier = 1
|
||||||
multiplier *= areAgeCompatible(lover1, lover2) ? 1 : 0.5
|
multiplier *= areAgeCompatible(profile1, profile2) ? 1 : 0.5
|
||||||
multiplier *= areRelationshipStyleCompatible(lover1, lover2) ? 1 : 0.5
|
multiplier *= areRelationshipStyleCompatible(profile1, profile2) ? 1 : 0.5
|
||||||
multiplier *= areWantKidsCompatible(lover1, lover2) ? 1 : 0.5
|
multiplier *= areWantKidsCompatible(profile1, profile2) ? 1 : 0.5
|
||||||
multiplier *= areLocationCompatible(lover1, lover2) ? 1 : 0.1
|
multiplier *= areLocationCompatible(profile1, profile2) ? 1 : 0.1
|
||||||
return multiplier
|
return multiplier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { LoverRow } from 'common/love/lover'
|
import { ProfileRow } from 'common/love/profile'
|
||||||
|
import {MAX_INT, MIN_INT} from "common/constants";
|
||||||
|
|
||||||
const isPreferredGender = (
|
const isPreferredGender = (
|
||||||
preferredGenders: string[] | undefined,
|
preferredGenders: string[] | undefined,
|
||||||
gender: string | undefined
|
gender: string | undefined
|
||||||
) => {
|
) => {
|
||||||
if (preferredGenders === undefined || gender === undefined) return true
|
// console.log('isPreferredGender', preferredGenders, gender)
|
||||||
|
if (preferredGenders === undefined || preferredGenders.length === 0 || gender === undefined) return true
|
||||||
|
|
||||||
// If simple gender preference, don't include non-binary.
|
// If simple gender preference, don't include non-binary.
|
||||||
if (
|
if (
|
||||||
@@ -16,52 +18,53 @@ const isPreferredGender = (
|
|||||||
return preferredGenders.includes(gender) || gender === 'non-binary'
|
return preferredGenders.includes(gender) || gender === 'non-binary'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const areGenderCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
export const areGenderCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||||
|
// console.log('areGenderCompatible', isPreferredGender(profile1.pref_gender, profile2.gender), isPreferredGender(profile2.pref_gender, profile1.gender))
|
||||||
return (
|
return (
|
||||||
isPreferredGender(lover1.pref_gender, lover2.gender) &&
|
isPreferredGender(profile1.pref_gender, profile2.gender) &&
|
||||||
isPreferredGender(lover2.pref_gender, lover1.gender)
|
isPreferredGender(profile2.pref_gender, profile1.gender)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const satisfiesAgeRange = (lover: LoverRow, age: number) => {
|
const satisfiesAgeRange = (profile: ProfileRow, age: number | null | undefined) => {
|
||||||
return age >= lover.pref_age_min && age <= lover.pref_age_max
|
return (age ?? MAX_INT) >= (profile.pref_age_min ?? MIN_INT) && (age ?? MIN_INT) <= (profile.pref_age_max ?? MAX_INT)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const areAgeCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
export const areAgeCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||||
return (
|
return (
|
||||||
satisfiesAgeRange(lover1, lover2.age) &&
|
satisfiesAgeRange(profile1, profile2.age) &&
|
||||||
satisfiesAgeRange(lover2, lover1.age)
|
satisfiesAgeRange(profile2, profile1.age)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const areLocationCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
export const areLocationCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||||
if (
|
if (
|
||||||
!lover1.city_latitude ||
|
!profile1.city_latitude ||
|
||||||
!lover2.city_latitude ||
|
!profile2.city_latitude ||
|
||||||
!lover1.city_longitude ||
|
!profile1.city_longitude ||
|
||||||
!lover2.city_longitude
|
!profile2.city_longitude
|
||||||
)
|
)
|
||||||
return lover1.city.trim().toLowerCase() === lover2.city.trim().toLowerCase()
|
return profile1.city.trim().toLowerCase() === profile2.city.trim().toLowerCase()
|
||||||
|
|
||||||
const latitudeDiff = Math.abs(lover1.city_latitude - lover2.city_latitude)
|
const latitudeDiff = Math.abs(profile1.city_latitude - profile2.city_latitude)
|
||||||
const longigudeDiff = Math.abs(lover1.city_longitude - lover2.city_longitude)
|
const longigudeDiff = Math.abs(profile1.city_longitude - profile2.city_longitude)
|
||||||
|
|
||||||
const root = (latitudeDiff ** 2 + longigudeDiff ** 2) ** 0.5
|
const root = (latitudeDiff ** 2 + longigudeDiff ** 2) ** 0.5
|
||||||
return root < 2.5
|
return root < 2.5
|
||||||
}
|
}
|
||||||
|
|
||||||
export const areRelationshipStyleCompatible = (
|
export const areRelationshipStyleCompatible = (
|
||||||
lover1: LoverRow,
|
profile1: ProfileRow,
|
||||||
lover2: LoverRow
|
profile2: ProfileRow
|
||||||
) => {
|
) => {
|
||||||
return lover1.pref_relation_styles.some((style) =>
|
return profile1.pref_relation_styles.some((style) =>
|
||||||
lover2.pref_relation_styles.includes(style)
|
profile2.pref_relation_styles.includes(style)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const areWantKidsCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
export const areWantKidsCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||||
const { wants_kids_strength: kids1 } = lover1
|
const { wants_kids_strength: kids1 } = profile1
|
||||||
const { wants_kids_strength: kids2 } = lover2
|
const { wants_kids_strength: kids2 } = profile2
|
||||||
|
|
||||||
if (kids1 === undefined || kids2 === undefined) return true
|
if (kids1 === undefined || kids2 === undefined) return true
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Row, run, SupabaseClient } from 'common/supabase/utils'
|
|
||||||
import { User } from 'common/user'
|
|
||||||
|
|
||||||
export type LoverRow = Row<'lovers'>
|
|
||||||
export type Lover = LoverRow & { user: User }
|
|
||||||
export const getLoverRow = async (userId: string, db: SupabaseClient) => {
|
|
||||||
console.log('getLoverRow', userId)
|
|
||||||
const res = await run(db.from('lovers').select('*').eq('user_id', userId))
|
|
||||||
return res.data[0]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { LoverRow } from 'common/love/lover'
|
import { ProfileRow } from 'common/love/profile'
|
||||||
import { buildOgUrl } from 'common/util/og'
|
import { buildOgUrl } from 'common/util/og'
|
||||||
|
|
||||||
// TODO: handle age, gender undefined better
|
// TODO: handle age, gender undefined better
|
||||||
@@ -8,21 +8,21 @@ export type LoveOgProps = {
|
|||||||
avatarUrl: string
|
avatarUrl: string
|
||||||
username: string
|
username: string
|
||||||
name: string
|
name: string
|
||||||
// lover props
|
// profile props
|
||||||
age: string
|
age: string
|
||||||
city: string
|
city: string
|
||||||
gender: string
|
gender: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLoveOgImageUrl(user: User, lover?: LoverRow | null) {
|
export function getLoveOgImageUrl(user: User, profile?: ProfileRow | null) {
|
||||||
const loveProps = {
|
const loveProps = {
|
||||||
avatarUrl: lover?.pinned_url,
|
avatarUrl: profile?.pinned_url,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
age: lover?.age.toString() ?? '25',
|
age: profile?.age?.toString() ?? '25',
|
||||||
city: lover?.city ?? 'Internet',
|
city: profile?.city ?? 'Internet',
|
||||||
gender: lover?.gender ?? '???',
|
gender: profile?.gender ?? '???',
|
||||||
} as LoveOgProps
|
} as LoveOgProps
|
||||||
|
|
||||||
return buildOgUrl(loveProps, 'lover', 'compassmeet.com')
|
return buildOgUrl(loveProps, 'profile')
|
||||||
}
|
}
|
||||||
|
|||||||
10
common/src/love/profile.ts
Normal file
10
common/src/love/profile.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Row, run, SupabaseClient } from 'common/supabase/utils'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
|
||||||
|
export type ProfileRow = Row<'profiles'>
|
||||||
|
export type Profile = ProfileRow & { user: User }
|
||||||
|
export const getProfileRow = async (userId: string, db: SupabaseClient) => {
|
||||||
|
console.log('getProfileRow', userId)
|
||||||
|
const res = await run(db.from('profiles').select('*').eq('user_id', userId))
|
||||||
|
return res.data[0]
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ export type Notification = {
|
|||||||
|
|
||||||
export const NOTIFICATION_TYPES_TO_SELECT = [
|
export const NOTIFICATION_TYPES_TO_SELECT = [
|
||||||
'new_match', // new match markets
|
'new_match', // new match markets
|
||||||
'comment_on_lover', // endorsements
|
'comment_on_profile', // endorsements
|
||||||
'love_like',
|
'love_like',
|
||||||
'love_ship',
|
'love_ship',
|
||||||
]
|
]
|
||||||
|
|||||||
87
common/src/searches.ts
Normal file
87
common/src/searches.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Define nice labels for each key
|
||||||
|
import {FilterFields, initialFilters} from "common/filters";
|
||||||
|
import {wantsKidsNames} from "common/wants-kids";
|
||||||
|
import {hasKidsNames} from "common/has-kids";
|
||||||
|
|
||||||
|
const filterLabels: Record<string, string> = {
|
||||||
|
geodbCityIds: "",
|
||||||
|
location: "",
|
||||||
|
name: "Searching",
|
||||||
|
genders: "",
|
||||||
|
pref_age_max: "Max age",
|
||||||
|
pref_age_min: "Min age",
|
||||||
|
has_kids: "",
|
||||||
|
wants_kids_strength: "Kids",
|
||||||
|
is_smoker: "",
|
||||||
|
pref_relation_styles: "Seeking",
|
||||||
|
pref_gender: "",
|
||||||
|
orderBy: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type locationType = {
|
||||||
|
location: {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
radius: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function formatFilters(filters: Partial<FilterFields>, location: locationType | null): String[] | null {
|
||||||
|
const entries: String[] = []
|
||||||
|
|
||||||
|
let ageEntry = null
|
||||||
|
let ageMin: number | undefined | null = filters.pref_age_min
|
||||||
|
if (ageMin == 18) ageMin = undefined
|
||||||
|
let ageMax = filters.pref_age_max;
|
||||||
|
if (ageMax == 100) ageMax = undefined
|
||||||
|
if (ageMin || ageMax) {
|
||||||
|
let text: string = 'Age: '
|
||||||
|
if (ageMin) text = `${text}${ageMin}`
|
||||||
|
if (ageMax) {
|
||||||
|
if (ageMin) {
|
||||||
|
text = `${text}-${ageMax}`
|
||||||
|
} else {
|
||||||
|
text = `${text}up to ${ageMax}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = `${text}+`
|
||||||
|
}
|
||||||
|
ageEntry = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
|
const typedKey = key as keyof FilterFields
|
||||||
|
|
||||||
|
if (value === undefined || value === null) return
|
||||||
|
if (typedKey == 'pref_age_min' || typedKey == 'pref_age_max' || typedKey == 'geodbCityIds' || typedKey == 'orderBy') return
|
||||||
|
if (Array.isArray(value) && value.length === 0) return
|
||||||
|
if (initialFilters[typedKey] === value) return
|
||||||
|
|
||||||
|
const label = filterLabels[typedKey] ?? key
|
||||||
|
|
||||||
|
let stringValue = value
|
||||||
|
if (key === 'has_kids') stringValue = hasKidsNames[value as number]
|
||||||
|
if (key === 'wants_kids_strength') stringValue = wantsKidsNames[value as number]
|
||||||
|
if (Array.isArray(value)) stringValue = value.join(', ')
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
const str = String(stringValue)
|
||||||
|
stringValue = str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = stringValue
|
||||||
|
|
||||||
|
entries.push(`${label}${label ? ': ' : ''}${display}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ageEntry) entries.push(ageEntry)
|
||||||
|
|
||||||
|
if (location?.location?.name) {
|
||||||
|
const locString = `${location?.location?.name} (${location?.radius}mi)`
|
||||||
|
entries.push(locString)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 0) return ['Anyone']
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user