mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 06:51:45 -04:00
Compare commits
140 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 |
16
.env.example
16
.env.example
@@ -1,14 +1,16 @@
|
||||
# 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.
|
||||
SUPABASE_DB_PASSWORD=
|
||||
NEXT_PUBLIC_SUPABASE_KEY=
|
||||
# For database write access (dev).
|
||||
# A 16-character password with digits and letters.
|
||||
SUPABASE_DB_PASSWORD=09wATRREfAzyL5pc
|
||||
|
||||
# For authentication.
|
||||
# Ask the project admin. Should start with "AIza".
|
||||
NEXT_PUBLIC_FIREBASE_API_KEY=
|
||||
# For Firebase access.
|
||||
# Open a GitHub issue with your contribution ideas and an admin will give you the 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.
|
||||
# You can change the port if needed.
|
||||
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -55,9 +55,27 @@ tsconfig.tsbuildinfo
|
||||
|
||||
*prisma/migrations
|
||||
martin
|
||||
email-preview
|
||||
.obsidian
|
||||
.idea
|
||||
*.last-run.json
|
||||
|
||||
*lock.hcl
|
||||
/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**:
|
||||
|
||||
```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
|
||||
|
||||
119
README.md
119
README.md
@@ -1,13 +1,13 @@
|
||||
|
||||
[](https://github.com/CompassMeet/Compass/actions/workflows/ci.yml)
|
||||
[](https://github.com/CompassMeet/Compass/actions/workflows/cd.yml)
|
||||
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
||||
[](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
|
||||

|
||||
|
||||
# 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
|
||||
|
||||
@@ -18,10 +18,15 @@ This repository provides the source code for [Compass](https://compassmeet.com),
|
||||
- Open source
|
||||
- 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
|
||||
|
||||
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] Set up PostgreSQL in Production with supabase
|
||||
- [x] Set up web hosting (vercel)
|
||||
@@ -30,20 +35,36 @@ A detailed description of the vision is available [here](https://martinbraquet.c
|
||||
- [x] Set up page listing all the profiles
|
||||
- [x] Search through most profile variables
|
||||
- [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
|
||||
|
||||
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 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)
|
||||
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||
- [ ] Clean up privacy notice (convert to Markdown)
|
||||
- [x] Clean up learn more page
|
||||
- [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
|
||||
|
||||
@@ -56,13 +77,13 @@ The web app is coded in Typescript using React as front-end. It includes:
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
git clone git@github.com:CompassMeet/Compass.git
|
||||
git clone https://github.com/<your-username>/Compass.git
|
||||
cd Compass
|
||||
```
|
||||
|
||||
@@ -70,7 +91,7 @@ Install `opentofu`, `docker`, and `yarn`. Try running this on Linux or macOS for
|
||||
```bash
|
||||
./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:
|
||||
```bash
|
||||
@@ -84,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
|
||||
- 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
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### Installing PostgreSQL
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Tests
|
||||
|
||||
Make sure the tests pass:
|
||||
```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
|
||||
|
||||
@@ -140,11 +130,38 @@ Start the development server:
|
||||
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!
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
FROM node:19-alpine
|
||||
FROM node:20-alpine
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install PM2 globally
|
||||
RUN yarn global add pm2
|
||||
|
||||
# Remove?
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# first get dependencies in for efficient docker layering
|
||||
# Fet dependencies in for efficient docker layering
|
||||
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 the PM2 ecosystem configuration
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
# 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.
|
||||
|
||||
### Requirements
|
||||
|
||||
You must have the `gcloud` CLI.
|
||||
|
||||
On MacOS:
|
||||
On macOS:
|
||||
|
||||
```bash
|
||||
brew install --cask google-cloud-sdk
|
||||
```
|
||||
|
||||
On Linux:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && sudo apt-get install google-cloud-sdk
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
gcloud init
|
||||
gcloud auth login
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
gcloud artifacts repositories create builds \
|
||||
--repository-format=docker \
|
||||
@@ -51,6 +56,20 @@ gcloud projects add-iam-policy-binding compass-130ba \
|
||||
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
|
||||
|
||||
* 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)"
|
||||
34.117.20.215
|
||||
```
|
||||
|
||||
Since Vercel manages your domain (`compassmeet.com`):
|
||||
|
||||
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:
|
||||
|
||||
| Type | Name | Value | TTL |
|
||||
| ---- | ---- | ------------ | ----- |
|
||||
|------|------|--------------|-------|
|
||||
| A | api | 34.123.45.67 | 600 s |
|
||||
|
||||
* `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`).
|
||||
* `curl -I` should return `200 OK` from your service.
|
||||
|
||||
|
||||
If SSL isn’t ready (may take 15 mins), check LB logs:
|
||||
|
||||
```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).
|
||||
|
||||
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).
|
||||
|
||||
@@ -111,13 +132,16 @@ In root directory, run the local api with hot reload, along with all the other b
|
||||
### Deploy
|
||||
|
||||
Run in this directory to deploy your code to the server.
|
||||
|
||||
```bash
|
||||
./deploy-api.sh prod
|
||||
```
|
||||
|
||||
### 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
|
||||
./ssh-api.sh prod
|
||||
```
|
||||
@@ -128,4 +152,11 @@ Useful commands once inside the server:
|
||||
sudo journalctl -u konlet-startup --no-pager -efb
|
||||
sudo docker logs -f $(sudo docker ps -alq)
|
||||
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!
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
{
|
||||
name: "api",
|
||||
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: {
|
||||
NODE_ENV: "production",
|
||||
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
|
||||
resource "google_compute_url_map" "api_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 {
|
||||
hosts = ["*"]
|
||||
@@ -185,9 +185,33 @@ resource "google_compute_url_map" "api_url_map" {
|
||||
path_matcher {
|
||||
name = "allpaths"
|
||||
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
|
||||
resource "google_compute_target_https_proxy" "api_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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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\"",
|
||||
"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: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",
|
||||
"verify": "yarn --cwd=../.. verify",
|
||||
"verify:dir": "npx eslint . --max-warnings 0",
|
||||
@@ -21,7 +21,7 @@
|
||||
"regen-types-dev": "cd ../supabase && make ENV=dev regen-types"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"main": "src/serve.ts",
|
||||
"dependencies": {
|
||||
@@ -45,26 +45,28 @@
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "1.11.4",
|
||||
"express": "4.18.1",
|
||||
"firebase-admin": "11.11.1",
|
||||
"firebase-admin": "13.5.0",
|
||||
"gcp-metadata": "6.1.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"pg-promise": "11.4.1",
|
||||
"posthog-node": "4.11.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "3.0.7",
|
||||
"resend": "4.1.2",
|
||||
"string-similarity": "4.0.4",
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twitter-api-v2": "1.15.0",
|
||||
"ws": "8.17.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "2.8.17",
|
||||
"@types/ws": "8.5.10",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0"
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/ws": "8.5.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,58 @@
|
||||
import { API, type APIPath } from 'common/api/schema'
|
||||
import { APIError, pathWithPrefix } from 'common/api/utils'
|
||||
import {API, type APIPath} from 'common/api/schema'
|
||||
import {APIError, pathWithPrefix} from 'common/api/utils'
|
||||
import cors from 'cors'
|
||||
import * as crypto from 'crypto'
|
||||
import express from 'express'
|
||||
import { type ErrorRequestHandler, type RequestHandler } from 'express'
|
||||
import { hrtime } from 'node:process'
|
||||
import { withMonitoringContext } from 'shared/monitoring/context'
|
||||
import { log } from 'shared/monitoring/log'
|
||||
import { metrics } from 'shared/monitoring/metrics'
|
||||
import { banUser } from './ban-user'
|
||||
import { blockUser, unblockUser } from './block-user'
|
||||
import { getCompatibleLoversHandler } from './compatible-lovers'
|
||||
import { createComment } from './create-comment'
|
||||
import { createCompatibilityQuestion } from './create-compatibility-question'
|
||||
import { createLover } from './create-lover'
|
||||
import { createUser } from './create-user'
|
||||
import { getCompatibilityQuestions } from './get-compatibililty-questions'
|
||||
import { getLikesAndShips } from './get-likes-and-ships'
|
||||
import { getLoverAnswers } from './get-lover-answers'
|
||||
import { getLovers } from './get-lovers'
|
||||
import { getSupabaseToken } from './get-supabase-token'
|
||||
import { getDisplayUser, getUser } from './get-user'
|
||||
import { getMe } from './get-me'
|
||||
import { hasFreeLike } from './has-free-like'
|
||||
import { health } from './health'
|
||||
import { typedEndpoint, type APIHandler } from './helpers/endpoint'
|
||||
import { hideComment } from './hide-comment'
|
||||
import { likeLover } from './like-lover'
|
||||
import { markAllNotifsRead } from './mark-all-notifications-read'
|
||||
import { removePinnedPhoto } from './remove-pinned-photo'
|
||||
import { report } from './report'
|
||||
import { searchLocation } from './search-location'
|
||||
import { searchNearCity } from './search-near-city'
|
||||
import { shipLovers } from './ship-lovers'
|
||||
import { starLover } from './star-lover'
|
||||
import { updateLover } from './update-lover'
|
||||
import { updateMe } from './update-me'
|
||||
import { deleteMe } from './delete-me'
|
||||
import { getCurrentPrivateUser } from './get-current-private-user'
|
||||
import { createPrivateUserMessage } from './create-private-user-message'
|
||||
import express, {type ErrorRequestHandler, type RequestHandler} from 'express'
|
||||
import {hrtime} from 'node:process'
|
||||
import {withMonitoringContext} from 'shared/monitoring/context'
|
||||
import {log} from 'shared/monitoring/log'
|
||||
import {metrics} from 'shared/monitoring/metrics'
|
||||
import {banUser} from './ban-user'
|
||||
import {blockUser, unblockUser} from './block-user'
|
||||
import {getCompatibleProfilesHandler} from './compatible-profiles'
|
||||
import {createComment} from './create-comment'
|
||||
import {createCompatibilityQuestion} from './create-compatibility-question'
|
||||
import {createProfile} from './create-profile'
|
||||
import {createUser} from './create-user'
|
||||
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
||||
import {getLikesAndShips} from './get-likes-and-ships'
|
||||
import {getProfileAnswers} from './get-profile-answers'
|
||||
import {getProfiles} from './get-profiles'
|
||||
import {getSupabaseToken} from './get-supabase-token'
|
||||
import {getDisplayUser, getUser} from './get-user'
|
||||
import {getMe} from './get-me'
|
||||
import {hasFreeLike} from './has-free-like'
|
||||
import {health} from './health'
|
||||
import {type APIHandler, typedEndpoint} from './helpers/endpoint'
|
||||
import {hideComment} from './hide-comment'
|
||||
import {likeProfile} from './like-profile'
|
||||
import {markAllNotifsRead} from './mark-all-notifications-read'
|
||||
import {removePinnedPhoto} from './remove-pinned-photo'
|
||||
import {report} from './report'
|
||||
import {searchLocation} from './search-location'
|
||||
import {searchNearCity} from './search-near-city'
|
||||
import {shipProfiles} from './ship-profiles'
|
||||
import {starProfile} from './star-profile'
|
||||
import {updateProfile} from './update-profile'
|
||||
import {updateMe} from './update-me'
|
||||
import {deleteMe} from './delete-me'
|
||||
import {getCurrentPrivateUser} from './get-current-private-user'
|
||||
import {createPrivateUserMessage} from './create-private-user-message'
|
||||
import {
|
||||
getChannelMemberships,
|
||||
getChannelMessages,
|
||||
getLastSeenChannelTime,
|
||||
setChannelLastSeenTime,
|
||||
} from 'api/get-private-messages'
|
||||
import { searchUsers } from './search-users'
|
||||
import { createPrivateUserMessageChannel } from './create-private-user-message-channel'
|
||||
import { leavePrivateUserMessageChannel } from './leave-private-user-message-channel'
|
||||
import { updatePrivateUserMessageChannel } from './update-private-user-message-channel'
|
||||
import { getNotifications } from './get-notifications'
|
||||
import { updateNotifSettings } from './update-notif-setting'
|
||||
import {searchUsers} from './search-users'
|
||||
import {createPrivateUserMessageChannel} from './create-private-user-message-channel'
|
||||
import {leavePrivateUserMessageChannel} from './leave-private-user-message-channel'
|
||||
import {updatePrivateUserMessageChannel} from './update-private-user-message-channel'
|
||||
import {getNotifications} from './get-notifications'
|
||||
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({})
|
||||
|
||||
@@ -66,15 +68,15 @@ const requestMonitoring: RequestHandler = (req, _res, next) => {
|
||||
const traceId = traceContext
|
||||
? traceContext.split('/')[0]
|
||||
: crypto.randomUUID()
|
||||
const context = { endpoint: req.path, traceId }
|
||||
const context = {endpoint: req.path, traceId}
|
||||
withMonitoringContext(context, () => {
|
||||
const startTs = hrtime.bigint()
|
||||
log(`${req.method} ${req.url}`)
|
||||
metrics.inc('http/request_count', { endpoint: req.path })
|
||||
metrics.inc('http/request_count', {endpoint: req.path})
|
||||
next()
|
||||
const endTs = hrtime.bigint()
|
||||
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) {
|
||||
log.info(error)
|
||||
if (!res.headersSent) {
|
||||
const output: { [k: string]: unknown } = { message: error.message }
|
||||
const output: { [k: string]: unknown } = {message: error.message}
|
||||
if (error.details != null) {
|
||||
output.details = error.details
|
||||
}
|
||||
@@ -91,7 +93,7 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
||||
} else {
|
||||
log.error(error)
|
||||
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()
|
||||
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)
|
||||
|
||||
const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
@@ -116,26 +134,26 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
'ban-user': banUser,
|
||||
report: report,
|
||||
'create-user': createUser,
|
||||
'create-lover': createLover,
|
||||
'create-profile': createProfile,
|
||||
me: getMe,
|
||||
'me/private': getCurrentPrivateUser,
|
||||
'me/update': updateMe,
|
||||
'update-notif-settings': updateNotifSettings,
|
||||
'me/delete': deleteMe,
|
||||
'update-lover': updateLover,
|
||||
'like-lover': likeLover,
|
||||
'ship-lovers': shipLovers,
|
||||
'update-profile': updateProfile,
|
||||
'like-profile': likeProfile,
|
||||
'ship-profiles': shipProfiles,
|
||||
'get-likes-and-ships': getLikesAndShips,
|
||||
'has-free-like': hasFreeLike,
|
||||
'star-lover': starLover,
|
||||
'get-lovers': getLovers,
|
||||
'get-lover-answers': getLoverAnswers,
|
||||
'star-profile': starProfile,
|
||||
'get-profiles': getProfiles,
|
||||
'get-profile-answers': getProfileAnswers,
|
||||
'get-compatibility-questions': getCompatibilityQuestions,
|
||||
'remove-pinned-photo': removePinnedPhoto,
|
||||
'create-comment': createComment,
|
||||
'hide-comment': hideComment,
|
||||
'create-compatibility-question': createCompatibilityQuestion,
|
||||
'compatible-lovers': getCompatibleLoversHandler,
|
||||
'compatible-profiles': getCompatibleProfilesHandler,
|
||||
'search-location': searchLocation,
|
||||
'search-near-city': searchNearCity,
|
||||
'create-private-user-message': createPrivateUserMessage,
|
||||
@@ -151,7 +169,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
Object.entries(handlers).forEach(([path, handler]) => {
|
||||
const api = API[path as APIPath]
|
||||
const cache = cacheController((api as any).cache)
|
||||
const url = '/' + pathWithPrefix(path as APIPath)
|
||||
const url = pathWithPrefix('/' + path as APIPath)
|
||||
|
||||
const apiRoute = [
|
||||
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) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).send()
|
||||
@@ -181,7 +220,7 @@ app.use(allowCorsUnrestricted, (req, res) => {
|
||||
.status(404)
|
||||
.set('Content-Type', 'application/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')
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
const comment = await pg.one<Row<'lover_comments'>>(
|
||||
`insert into lover_comments (user_id, user_name, user_username, user_avatar_url, on_user_id, content, reply_to_comment_id)
|
||||
const comment = await pg.one<Row<'profile_comments'>>(
|
||||
`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 *`,
|
||||
[
|
||||
creator.id,
|
||||
@@ -46,7 +46,7 @@ export const createComment: APIHandler<'create-comment'> = async (
|
||||
]
|
||||
)
|
||||
if (onUser.id !== creator.id)
|
||||
await createNewCommentOnLoverNotification(
|
||||
await createNewCommentOnProfileNotification(
|
||||
onUser,
|
||||
creator,
|
||||
richTextToString(content),
|
||||
@@ -84,7 +84,7 @@ const validateComment = async (
|
||||
return { content, creator }
|
||||
}
|
||||
|
||||
const createNewCommentOnLoverNotification = async (
|
||||
const createNewCommentOnProfileNotification = async (
|
||||
onUser: User,
|
||||
creator: User,
|
||||
sourceText: string,
|
||||
@@ -104,7 +104,7 @@ const createNewCommentOnLoverNotification = async (
|
||||
createdTime: Date.now(),
|
||||
isSeen: false,
|
||||
sourceId: commentId.toString(),
|
||||
sourceType: 'comment_on_lover',
|
||||
sourceType: 'comment_on_profile',
|
||||
sourceUpdateType: 'created',
|
||||
sourceUserName: creator.name,
|
||||
sourceUserUsername: creator.username,
|
||||
|
||||
@@ -8,11 +8,11 @@ import { updateUser } from 'shared/supabase/users'
|
||||
import { tryCatch } from 'common/util/try-catch'
|
||||
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 { 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,
|
||||
])
|
||||
)
|
||||
@@ -31,7 +31,7 @@ export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
|
||||
console.log('body', body)
|
||||
|
||||
const { data, error } = await tryCatch(
|
||||
insert(pg, 'lovers', { user_id: auth.uid, ...body })
|
||||
insert(pg, 'profiles', { user_id: auth.uid, ...body })
|
||||
)
|
||||
|
||||
if (error) {
|
||||
@@ -40,7 +40,7 @@ export const createLover: APIHandler<'create-lover'> = async (body, auth) => {
|
||||
}
|
||||
|
||||
log('Created user', data)
|
||||
await track(user.id, 'create lover', { username: user.username })
|
||||
await track(user.id, 'create profile', { username: user.username })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export const createUser: APIHandler<'create-user'> = async (
|
||||
auth,
|
||||
req
|
||||
) => {
|
||||
const { deviceToken: preDeviceToken, adminToken } = props
|
||||
const { deviceToken: preDeviceToken } = props
|
||||
const firebaseUser = await admin.auth().getUser(auth.uid)
|
||||
|
||||
const testUserAKAEmailPasswordUser =
|
||||
@@ -123,7 +123,7 @@ export const createUser: APIHandler<'create-user'> = async (
|
||||
log('created user ', { username: user.username, firebaseId: auth.uid })
|
||||
|
||||
const continuation = async () => {
|
||||
await track(auth.uid, 'create lover', { username: user.username })
|
||||
await track(auth.uid, 'create profile', { username: user.username })
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,11 +22,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
`
|
||||
select target_id, love_likes.created_time
|
||||
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
|
||||
where creator_id = $1
|
||||
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)
|
||||
order by created_time desc
|
||||
`,
|
||||
@@ -44,11 +44,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
`
|
||||
select creator_id, love_likes.created_time
|
||||
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
|
||||
where target_id = $1
|
||||
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)
|
||||
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 as target_id
|
||||
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
|
||||
where target2_id = $1
|
||||
and lovers.looking_for_matches
|
||||
and lovers.pinned_url is not null
|
||||
and profiles.looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||
|
||||
union all
|
||||
@@ -84,11 +84,11 @@ export const getLikesAndShipsMain = async (userId: string) => {
|
||||
target1_id, target2_id, creator_id, love_ships.created_time,
|
||||
target2_id as target_id
|
||||
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
|
||||
where target1_id = $1
|
||||
and lovers.looking_for_matches
|
||||
and lovers.pinned_url is not null
|
||||
and profiles.looking_for_matches
|
||||
and profiles.pinned_url is not null
|
||||
and (users.data->>'isBannedFromPosting' != 'true' or users.data->>'isBannedFromPosting' is null)
|
||||
`,
|
||||
[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)) || '%' or lower(bio::text) 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 { Row } from 'common/supabase/utils'
|
||||
|
||||
export const getLoverAnswers: APIHandler<'get-lover-answers'> = async (
|
||||
export const getProfileAnswers: APIHandler<'get-profile-answers'> = async (
|
||||
props,
|
||||
_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
|
||||
) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
const comment = await pg.oneOrNone<Row<'lover_comments'>>(
|
||||
`select * from lover_comments where id = $1`,
|
||||
const comment = await pg.oneOrNone<Row<'profile_comments'>>(
|
||||
`select * from profile_comments where id = $1`,
|
||||
[commentId]
|
||||
)
|
||||
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')
|
||||
}
|
||||
|
||||
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,
|
||||
hide,
|
||||
])
|
||||
|
||||
@@ -6,7 +6,7 @@ import { log } from 'shared/utils'
|
||||
import { tryCatch } from 'common/util/try-catch'
|
||||
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 creatorId = auth.uid
|
||||
|
||||
@@ -17,7 +17,7 @@ export const removePinnedPhoto: APIHandler<'remove-pinned-photo'> = async (
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
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) {
|
||||
|
||||
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 { getLocalEnv, initAdmin } from 'shared/init-admin'
|
||||
import { loadSecretsToEnv, getServiceAccountCredentials } from 'common/secrets'
|
||||
import { LOCAL_DEV, log } from 'shared/utils'
|
||||
import { METRIC_WRITER } from 'shared/monitoring/metric-writer'
|
||||
import { listen as webSocketListen } from 'shared/websockets/server'
|
||||
import {getLocalEnv, initAdmin} from 'shared/init-admin'
|
||||
import {loadSecretsToEnv, getServiceAccountCredentials} from 'common/secrets'
|
||||
import {log} from 'shared/utils'
|
||||
import {LOCAL_DEV} from "common/envs/constants";
|
||||
import {METRIC_WRITER} from 'shared/monitoring/metric-writer'
|
||||
import {listen as webSocketListen} from 'shared/websockets/server'
|
||||
|
||||
log('Api server starting up....')
|
||||
|
||||
@@ -19,12 +20,12 @@ if (LOCAL_DEV) {
|
||||
|
||||
METRIC_WRITER.start()
|
||||
|
||||
import { app } from './app'
|
||||
import {app} from './app'
|
||||
|
||||
const credentials = LOCAL_DEV
|
||||
? getServiceAccountCredentials(getLocalEnv())
|
||||
: // No explicit credentials needed for deployed service.
|
||||
undefined
|
||||
undefined
|
||||
|
||||
const startupProcess = async () => {
|
||||
await loadSecretsToEnv(credentials)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { log } from 'shared/utils'
|
||||
import { tryCatch } from 'common/util/try-catch'
|
||||
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 creatorId = auth.uid
|
||||
|
||||
@@ -5,7 +5,7 @@ import { tryCatch } from 'common/util/try-catch'
|
||||
import { Row } from 'common/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 creatorId = auth.uid
|
||||
|
||||
@@ -7,25 +7,25 @@ import { tryCatch } from 'common/util/try-catch'
|
||||
import { update } from 'shared/supabase/utils'
|
||||
import { type Row } from 'common/supabase/utils'
|
||||
|
||||
export const updateLover: APIHandler<'update-lover'> = async (
|
||||
export const updateProfile: APIHandler<'update-profile'> = async (
|
||||
parsedBody,
|
||||
auth
|
||||
) => {
|
||||
log('parsedBody', parsedBody)
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
const { data: existingLover } = await tryCatch(
|
||||
pg.oneOrNone<Row<'lovers'>>('select * from lovers where user_id = $1', [
|
||||
const { data: existingProfile } = await tryCatch(
|
||||
pg.oneOrNone<Row<'profiles'>>('select * from profiles where user_id = $1', [
|
||||
auth.uid,
|
||||
])
|
||||
)
|
||||
|
||||
if (!existingLover) {
|
||||
throw new APIError(404, 'Lover not found')
|
||||
if (!existingProfile) {
|
||||
throw new APIError(404, 'Profile not found')
|
||||
}
|
||||
|
||||
!parsedBody.last_online_time &&
|
||||
log('Updating lover', { userId: auth.uid, parsedBody })
|
||||
log('Updating profile', { userId: auth.uid, parsedBody })
|
||||
|
||||
await removePinnedUrlFromPhotoUrls(parsedBody)
|
||||
if (parsedBody.avatar_url) {
|
||||
@@ -33,12 +33,12 @@ export const updateLover: APIHandler<'update-lover'> = async (
|
||||
}
|
||||
|
||||
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) {
|
||||
log('Error updating lover', error)
|
||||
throw new APIError(500, 'Error updating lover')
|
||||
log('Error updating profile', error)
|
||||
throw new APIError(500, 'Error updating profile')
|
||||
}
|
||||
|
||||
return data
|
||||
@@ -9,24 +9,47 @@
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "esnext",
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"common/*": ["../../common/src/*", "../../../common/lib/*"],
|
||||
"shared/*": ["../shared/src/*", "../../shared/lib/*"],
|
||||
"email/*": ["../email/emails/*", "../../email/lib/*"],
|
||||
"api/*": ["./src/*"]
|
||||
"common/*": [
|
||||
"../../common/src/*",
|
||||
"../../../common/lib/*"
|
||||
],
|
||||
"shared/*": [
|
||||
"../shared/src/*",
|
||||
"../../shared/lib/*"
|
||||
],
|
||||
"email/*": [
|
||||
"../email/emails/*",
|
||||
"../../email/lib/*"
|
||||
],
|
||||
"api/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"require": ["tsconfig-paths/register"]
|
||||
"require": [
|
||||
"tsconfig-paths/register"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../../common" },
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../email" }
|
||||
{
|
||||
"path": "../../common"
|
||||
},
|
||||
{
|
||||
"path": "../shared"
|
||||
},
|
||||
{
|
||||
"path": "../email"
|
||||
}
|
||||
],
|
||||
"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:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
# or
|
||||
yarn
|
||||
yarn install
|
||||
```
|
||||
|
||||
Then, run the development server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
# or
|
||||
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,42 +1,43 @@
|
||||
import { PrivateUser, User } from 'common/user'
|
||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
||||
import { sendEmail } from './send-email'
|
||||
import { NewMatchEmail } from '../new-match'
|
||||
import { NewMessageEmail } from '../new-message'
|
||||
import { NewEndorsementEmail } from '../new-endorsement'
|
||||
import { Test } from '../test'
|
||||
import { getLover } from 'shared/love/supabase'
|
||||
import {renderToStaticMarkup} from "react-dom/server";
|
||||
import {PrivateUser, User} from 'common/user'
|
||||
import {getNotificationDestinationsForUser} from 'common/user-notification-preferences'
|
||||
import {sendEmail} from './send-email'
|
||||
import {NewMessageEmail} from '../new-message'
|
||||
import {NewEndorsementEmail} from '../new-endorsement'
|
||||
import {Test} from '../test'
|
||||
import {getProfile} from 'shared/love/supabase'
|
||||
import { render } from "@react-email/render"
|
||||
import {MatchesType} from "common/love/bookmarked_searches";
|
||||
import NewSearchAlertsEmail from "email/new-search_alerts";
|
||||
|
||||
const from = 'Compass <no-reply@compassmeet.com>'
|
||||
|
||||
export const sendNewMatchEmail = async (
|
||||
privateUser: PrivateUser,
|
||||
matchedWithUser: User
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_match'
|
||||
)
|
||||
if (!privateUser.email || !sendToEmail) return
|
||||
const lover = await getLover(privateUser.id)
|
||||
if (!lover) return
|
||||
|
||||
return await sendEmail({
|
||||
from,
|
||||
subject: `You have a new match!`,
|
||||
to: privateUser.email,
|
||||
react: (
|
||||
<NewMatchEmail
|
||||
onUser={lover.user}
|
||||
email={privateUser.email}
|
||||
matchedWithUser={matchedWithUser}
|
||||
matchedLover={lover}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
// export const sendNewMatchEmail = async (
|
||||
// privateUser: PrivateUser,
|
||||
// matchedWithUser: User
|
||||
// ) => {
|
||||
// const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
// privateUser,
|
||||
// 'new_match'
|
||||
// )
|
||||
// if (!privateUser.email || !sendToEmail) return
|
||||
// const profile = await getProfile(privateUser.id)
|
||||
// if (!profile) return
|
||||
//
|
||||
// return await sendEmail({
|
||||
// from,
|
||||
// subject: `You have a new match!`,
|
||||
// to: privateUser.email,
|
||||
// react: (
|
||||
// <NewMatchEmail
|
||||
// onUser={profile.user}
|
||||
// email={privateUser.email}
|
||||
// matchedWithUser={matchedWithUser}
|
||||
// matchedProfile={profile}
|
||||
// unsubscribeUrl={unsubscribeUrl}
|
||||
// />
|
||||
// ),
|
||||
// })
|
||||
// }
|
||||
|
||||
export const sendNewMessageEmail = async (
|
||||
privateUser: PrivateUser,
|
||||
@@ -44,27 +45,27 @@ export const sendNewMessageEmail = async (
|
||||
toUser: User,
|
||||
channelId: number
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_message'
|
||||
)
|
||||
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')
|
||||
return
|
||||
}
|
||||
|
||||
console.log({
|
||||
return await sendEmail({
|
||||
from,
|
||||
subject: `${fromUser.name} sent you a message!`,
|
||||
to: privateUser.email,
|
||||
html: renderToStaticMarkup(
|
||||
html: await render(
|
||||
<NewMessageEmail
|
||||
fromUser={fromUser}
|
||||
fromUserLover={lover}
|
||||
fromUserProfile={profile}
|
||||
toUser={toUser}
|
||||
channelId={channelId}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
@@ -72,19 +73,30 @@ export const sendNewMessageEmail = async (
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
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: `${fromUser.name} sent you a message!`,
|
||||
to: privateUser.email,
|
||||
html: renderToStaticMarkup(
|
||||
<NewMessageEmail
|
||||
fromUser={fromUser}
|
||||
fromUserLover={lover}
|
||||
subject: `People aligned with your values just joined`,
|
||||
to: email,
|
||||
html: await render(
|
||||
<NewSearchAlertsEmail
|
||||
toUser={toUser}
|
||||
channelId={channelId}
|
||||
matches={matches}
|
||||
unsubscribeUrl={unsubscribeUrl}
|
||||
email={privateUser.email}
|
||||
email={email}
|
||||
/>
|
||||
),
|
||||
})
|
||||
@@ -96,7 +108,7 @@ export const sendNewEndorsementEmail = async (
|
||||
onUser: User,
|
||||
text: string
|
||||
) => {
|
||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||
const {sendToEmail, unsubscribeUrl} = getNotificationDestinationsForUser(
|
||||
privateUser,
|
||||
'new_endorsement'
|
||||
)
|
||||
@@ -106,7 +118,7 @@ export const sendNewEndorsementEmail = async (
|
||||
from,
|
||||
subject: `${fromUser.name} just endorsed you!`,
|
||||
to: privateUser.email,
|
||||
react: (
|
||||
html: await render(
|
||||
<NewEndorsementEmail
|
||||
fromUser={fromUser}
|
||||
onUser={onUser}
|
||||
@@ -123,6 +135,6 @@ export const sendTestEmail = async (toEmail: string) => {
|
||||
from,
|
||||
subject: 'Test email from Compass',
|
||||
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'
|
||||
|
||||
// for email template testing
|
||||
@@ -27,11 +27,12 @@ export const sinclairUser: User = {
|
||||
},
|
||||
}
|
||||
|
||||
export const sinclairLover: LoverRow = {
|
||||
export const sinclairProfile: ProfileRow = {
|
||||
id: 55,
|
||||
user_id: '0k1suGSJKVUnHbCPEhHNpgZPkUP2',
|
||||
created_time: '2023-10-27T00:41:59.851776+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',
|
||||
gender: '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,
|
||||
user_id: '5LZ4LgYuySdL1huCWe7bti02ghx2',
|
||||
created_time: '2023-10-21T21:18:26.691211+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',
|
||||
gender: 'male',
|
||||
pref_gender: ['female'],
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||
import {DOMAIN} from 'common/envs/constants'
|
||||
import {type LoverRow} from 'common/love/lover'
|
||||
import {type ProfileRow} from 'common/love/profile'
|
||||
import {type User} from 'common/user'
|
||||
import {jamesLover, jamesUser, sinclairUser} from './functions/mock'
|
||||
import {jamesProfile, jamesUser, sinclairUser} from './functions/mock'
|
||||
import {Footer} from "email/utils";
|
||||
|
||||
interface NewMatchEmailProps {
|
||||
onUser: User
|
||||
matchedWithUser: User
|
||||
matchedLover: LoverRow
|
||||
matchedProfile: ProfileRow
|
||||
unsubscribeUrl: string
|
||||
email?: string
|
||||
}
|
||||
@@ -16,12 +16,12 @@ interface NewMatchEmailProps {
|
||||
export const NewMatchEmail = ({
|
||||
onUser,
|
||||
matchedWithUser,
|
||||
matchedLover,
|
||||
// matchedProfile,
|
||||
unsubscribeUrl,
|
||||
email
|
||||
}: NewMatchEmailProps) => {
|
||||
const name = onUser.name.split(' ')[0]
|
||||
// const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedLover)
|
||||
// const userImgSrc = getLoveOgImageUrl(matchedWithUser, matchedProfile)
|
||||
const userUrl = `https://${DOMAIN}/${matchedWithUser.username}`
|
||||
|
||||
return (
|
||||
@@ -72,7 +72,7 @@ export const NewMatchEmail = ({
|
||||
NewMatchEmail.PreviewProps = {
|
||||
onUser: sinclairUser,
|
||||
matchedWithUser: jamesUser,
|
||||
matchedLover: jamesLover,
|
||||
matchedProfile: jamesProfile,
|
||||
email: 'someone@gmail.com',
|
||||
unsubscribeUrl: 'https://compassmeet.com/unsubscribe',
|
||||
} as NewMatchEmailProps
|
||||
@@ -88,11 +88,11 @@ const container = {
|
||||
maxWidth: '600px',
|
||||
}
|
||||
|
||||
const logoContainer = {
|
||||
padding: '20px 0px 5px 0px',
|
||||
textAlign: 'center' as const,
|
||||
backgroundColor: '#ffffff',
|
||||
}
|
||||
// const logoContainer = {
|
||||
// padding: '20px 0px 5px 0px',
|
||||
// textAlign: 'center' as const,
|
||||
// backgroundColor: '#ffffff',
|
||||
// }
|
||||
|
||||
const content = {
|
||||
backgroundColor: '#ffffff',
|
||||
@@ -112,9 +112,9 @@ const imageContainer = {
|
||||
margin: '20px 0',
|
||||
}
|
||||
|
||||
const profileImage = {
|
||||
// border: '1px solid #ec489a',
|
||||
}
|
||||
// const profileImage = {
|
||||
// // border: '1px solid #ec489a',
|
||||
// }
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#4887ec',
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
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'
|
||||
import {button, container, content, Footer, imageContainer, main, paragraph, profileImage} from "email/utils";
|
||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||
import {type User} from 'common/user'
|
||||
import {type ProfileRow} from 'common/love/profile'
|
||||
import {jamesProfile, jamesUser, sinclairUser,} from './functions/mock'
|
||||
import {DOMAIN} from 'common/envs/constants'
|
||||
import {button, container, content, Footer, imageContainer, main, paragraph} from "email/utils";
|
||||
|
||||
interface NewMessageEmailProps {
|
||||
fromUser: User
|
||||
fromUserLover: LoverRow
|
||||
fromUserProfile: ProfileRow
|
||||
toUser: User
|
||||
channelId: number
|
||||
unsubscribeUrl: string
|
||||
@@ -32,21 +15,21 @@ interface NewMessageEmailProps {
|
||||
}
|
||||
|
||||
export const NewMessageEmail = ({
|
||||
fromUser,
|
||||
fromUserLover,
|
||||
toUser,
|
||||
channelId,
|
||||
unsubscribeUrl,
|
||||
email,
|
||||
}: NewMessageEmailProps) => {
|
||||
fromUser,
|
||||
// fromUserProfile,
|
||||
toUser,
|
||||
channelId,
|
||||
unsubscribeUrl,
|
||||
email,
|
||||
}: NewMessageEmailProps) => {
|
||||
const name = toUser.name.split(' ')[0]
|
||||
const creatorName = fromUser.name
|
||||
const messagesUrl = `https://${DOMAIN}/messages/${channelId}`
|
||||
const userImgSrc = getLoveOgImageUrl(fromUser, fromUserLover)
|
||||
// const userImgSrc = getLoveOgImageUrl(fromUser, fromUserProfile)
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Head/>
|
||||
<Preview>New message from {creatorName}</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
@@ -90,7 +73,7 @@ export const NewMessageEmail = ({
|
||||
|
||||
NewMessageEmail.PreviewProps = {
|
||||
fromUser: jamesUser,
|
||||
fromUserLover: jamesLover,
|
||||
fromUserProfile: jamesProfile,
|
||||
toUser: sinclairUser,
|
||||
channelId: 1,
|
||||
email: 'someone@gmail.com',
|
||||
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Link, Row, Section, Text} from "@react-email/components";
|
||||
import {Column, Img, Link, Row, Section, Text} from "@react-email/components";
|
||||
|
||||
interface Props {
|
||||
email?: string
|
||||
@@ -10,26 +10,54 @@ export const Footer = ({
|
||||
unsubscribeUrl,
|
||||
}: Props) => {
|
||||
return <Section style={footer}>
|
||||
<hr style={{border: 'none', borderTop: '1px solid #e0e0e0', margin: '20px 0'}}/>
|
||||
<hr style={{border: 'none', borderTop: '1px solid #e0e0e0', margin: '10px 0'}}/>
|
||||
<Row>
|
||||
<Text style={footerText}>
|
||||
Compass © {new Date().getFullYear()}
|
||||
<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>
|
||||
{/*<Row>*/}
|
||||
{/* <Link href="https://github.com/CompassMeet/Compass">*/}
|
||||
{/* <TbBrandGithub size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://discord.gg/8Vd7jzqjun">*/}
|
||||
{/* <TbBrandDiscord size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://patreon.com/CompassMeet">*/}
|
||||
{/* <TbBrandPatreon size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/* <Link href="https://www.paypal.com/paypalme/MartinBraquet">*/}
|
||||
{/* <TbBrandPaypal size={36} color={'black'}/>*/}
|
||||
{/* </Link>*/}
|
||||
{/*</Row>*/}
|
||||
<Text style={footerText}>
|
||||
|
||||
<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
|
||||
@@ -86,11 +114,11 @@ export const content = {
|
||||
}
|
||||
|
||||
export const paragraph = {
|
||||
fontSize: '18px',
|
||||
// fontSize: '12px',
|
||||
lineHeight: '24px',
|
||||
margin: '10px 0',
|
||||
color: '#000000',
|
||||
fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
// fontFamily: 'Arial, Helvetica, sans-serif',
|
||||
}
|
||||
|
||||
export const imageContainer = {
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.33",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "3.0.7",
|
||||
"react-icons": "5.5.0",
|
||||
"resend": "4.1.2"
|
||||
"resend": "4.1.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "9.0.4",
|
||||
|
||||
@@ -8,11 +8,11 @@ import { chunk } from 'lodash'
|
||||
runScript(async ({ pg }) => {
|
||||
const directClient = createSupabaseDirectClient()
|
||||
|
||||
// Get all users and their corresponding lovers
|
||||
// Get all users and their corresponding profiles
|
||||
const users = await directClient.manyOrNone(`
|
||||
select u.id, u.data, l.twitter
|
||||
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')
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
select,
|
||||
from,
|
||||
where,
|
||||
} from '../shared/src/supabase/sql-builder'
|
||||
} from 'shared/supabase/sql-builder'
|
||||
import { SupabaseDirectClient } from 'shared/supabase/init'
|
||||
|
||||
runScript(async ({ pg }) => {
|
||||
@@ -27,7 +27,7 @@ const getNodes = async (pg: SupabaseDirectClient, nodeName: string) => {
|
||||
console.log(`\nSearching comments for ${nodeName}...`)
|
||||
const commentQuery = renderSql(
|
||||
select('id, user_id, on_user_id, content'),
|
||||
from('lover_comments'),
|
||||
from('profile_comments'),
|
||||
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeName}")')`)
|
||||
)
|
||||
const comments = await pg.manyOrNone(commentQuery)
|
||||
@@ -59,7 +59,7 @@ const getNodes = async (pg: SupabaseDirectClient, nodeName: string) => {
|
||||
console.log(`\nSearching profiles for ${nodeName}...`)
|
||||
const users = renderSql(
|
||||
select('user_id, bio'),
|
||||
from('lovers'),
|
||||
from('profiles'),
|
||||
where(`jsonb_path_exists(bio::jsonb, '$.**.type ? (@ == "${nodeName}")')`)
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
select,
|
||||
from,
|
||||
where,
|
||||
} from '../shared/src/supabase/sql-builder'
|
||||
} from 'shared/supabase/sql-builder'
|
||||
import { type JSONContent } from '@tiptap/core'
|
||||
|
||||
const removeNodesOfType = (
|
||||
@@ -33,7 +33,7 @@ runScript(async ({ pg }) => {
|
||||
console.log('\nSearching comments for linkPreviews...')
|
||||
const commentQuery = renderSql(
|
||||
select('id, content'),
|
||||
from('lover_comments'),
|
||||
from('profile_comments'),
|
||||
where(`jsonb_path_exists(content, '$.**.type ? (@ == "${nodeType}")')`)
|
||||
)
|
||||
const comments = await pg.manyOrNone(commentQuery)
|
||||
@@ -45,7 +45,7 @@ runScript(async ({ pg }) => {
|
||||
console.log('before', comment.content)
|
||||
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,
|
||||
comment.id,
|
||||
])
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@tiptap/html": "2.3.2",
|
||||
"colors": "1.4.0",
|
||||
"dayjs": "1.11.4",
|
||||
"firebase-admin": "11.11.1",
|
||||
"firebase-admin": "13.5.0",
|
||||
"gcp-metadata": "6.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"pg-promise": "11.4.1",
|
||||
|
||||
@@ -4,15 +4,15 @@ import { createSupabaseDirectClient } from './supabase/init'
|
||||
import { getNotificationDestinationsForUser } from 'common/user-notification-preferences'
|
||||
import { Notification } from 'common/notifications'
|
||||
import { insertNotificationToSupabase } from './supabase/notifications'
|
||||
import { getLover } from './love/supabase'
|
||||
import { getProfile } from './love/supabase'
|
||||
|
||||
export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
||||
const { creator_id, target_id, like_id } = like
|
||||
|
||||
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(
|
||||
targetPrivateUser,
|
||||
@@ -30,9 +30,9 @@ export const createLoveLikeNotification = async (like: Row<'love_likes'>) => {
|
||||
sourceId: like_id,
|
||||
sourceType: 'love_like',
|
||||
sourceUpdateType: 'created',
|
||||
sourceUserName: lover.user.name,
|
||||
sourceUserUsername: lover.user.username,
|
||||
sourceUserAvatarUrl: lover.pinned_url ?? lover.user.avatarUrl,
|
||||
sourceUserName: profile.user.name,
|
||||
sourceUserUsername: profile.user.username,
|
||||
sourceUserAvatarUrl: profile.pinned_url ?? profile.user.avatarUrl,
|
||||
sourceText: '',
|
||||
}
|
||||
const pg = createSupabaseDirectClient()
|
||||
@@ -48,13 +48,13 @@ export const createLoveShipNotification = async (
|
||||
|
||||
const creator = await getUser(creator_id)
|
||||
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', {
|
||||
creator,
|
||||
targetPrivateUser,
|
||||
lover,
|
||||
profile,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -75,9 +75,9 @@ export const createLoveShipNotification = async (
|
||||
sourceId: ship_id,
|
||||
sourceType: 'love_ship',
|
||||
sourceUpdateType: 'created',
|
||||
sourceUserName: lover.user.name,
|
||||
sourceUserUsername: lover.user.username,
|
||||
sourceUserAvatarUrl: lover.pinned_url ?? lover.user.avatarUrl,
|
||||
sourceUserName: profile.user.name,
|
||||
sourceUserUsername: profile.user.username,
|
||||
sourceUserAvatarUrl: profile.pinned_url ?? profile.user.avatarUrl,
|
||||
sourceText: '',
|
||||
data: {
|
||||
creatorId: creator_id,
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
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 { Row } from 'common/supabase/utils'
|
||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||
|
||||
export type LoverAndUserRow = LoverRow & {
|
||||
export type ProfileAndUserRow = ProfileRow & {
|
||||
name: string
|
||||
username: string
|
||||
user: any
|
||||
}
|
||||
|
||||
export function convertRow(row: LoverAndUserRow): Lover
|
||||
export function convertRow(row: LoverAndUserRow | undefined): Lover | null {
|
||||
export function convertRow(row: ProfileAndUserRow): Profile
|
||||
export function convertRow(row: ProfileAndUserRow | undefined): Profile | null {
|
||||
if (!row) return null
|
||||
|
||||
return {
|
||||
...row,
|
||||
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()
|
||||
return await pg.oneOrNone(
|
||||
`
|
||||
select
|
||||
${LOVER_COLS}
|
||||
from
|
||||
lovers
|
||||
profiles
|
||||
join
|
||||
users on users.id = lovers.user_id
|
||||
users on users.id = profiles.user_id
|
||||
where
|
||||
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()
|
||||
return await pg.map(
|
||||
`
|
||||
select
|
||||
${LOVER_COLS}
|
||||
from
|
||||
lovers
|
||||
profiles
|
||||
join
|
||||
users on users.id = lovers.user_id
|
||||
users on users.id = profiles.user_id
|
||||
where
|
||||
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 lovers = await pg.map(
|
||||
const profiles = await pg.map(
|
||||
`
|
||||
select
|
||||
${LOVER_COLS}
|
||||
from lovers
|
||||
from profiles
|
||||
join
|
||||
users on users.id = lovers.user_id
|
||||
users on users.id = profiles.user_id
|
||||
where
|
||||
user_id != $(user_id)
|
||||
and looking_for_matches
|
||||
and (data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' 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
|
||||
)
|
||||
return lovers.filter((l: Lover) => areGenderCompatible(lover, l))
|
||||
return profiles.filter((l: Profile) => areGenderCompatible(profile, l))
|
||||
}
|
||||
|
||||
export const getCompatibleLovers = async (
|
||||
lover: LoverRow,
|
||||
export const getCompatibleProfiles = async (
|
||||
profile: ProfileRow,
|
||||
radiusKm: number | undefined
|
||||
) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
@@ -89,9 +89,9 @@ export const getCompatibleLovers = async (
|
||||
`
|
||||
select
|
||||
${LOVER_COLS}
|
||||
from lovers
|
||||
from profiles
|
||||
join
|
||||
users on users.id = lovers.user_id
|
||||
users on users.id = profiles.user_id
|
||||
where
|
||||
user_id != $(user_id)
|
||||
and looking_for_matches
|
||||
@@ -99,19 +99,19 @@ export const getCompatibleLovers = async (
|
||||
and (data->>'userDeleted' != 'true' or data->>'userDeleted' is null)
|
||||
|
||||
-- Gender
|
||||
and (lovers.gender = any($(pref_gender)) or lovers.gender = 'non-binary')
|
||||
and ($(gender) = any(lovers.pref_gender) or $(gender) = 'non-binary')
|
||||
and (profiles.gender = any($(pref_gender)) or profiles.gender = 'non-binary')
|
||||
and ($(gender) = any(profiles.pref_gender) or $(gender) = 'non-binary')
|
||||
|
||||
-- Age
|
||||
and lovers.age >= $(pref_age_min)
|
||||
and lovers.age <= $(pref_age_max)
|
||||
and $(age) >= lovers.pref_age_min
|
||||
and $(age) <= lovers.pref_age_max
|
||||
and profiles.age >= $(pref_age_min)
|
||||
and profiles.age <= $(pref_age_max)
|
||||
and $(age) >= profiles.pref_age_min
|
||||
and $(age) <= profiles.pref_age_max
|
||||
|
||||
-- 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import {
|
||||
createSupabaseDirectClient,
|
||||
SupabaseDirectClient,
|
||||
} from 'shared/supabase/init'
|
||||
import {createSupabaseDirectClient, SupabaseDirectClient,} from 'shared/supabase/init'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { convertPrivateUser, convertUser } from 'common/supabase/users'
|
||||
import { log, type Logger } from 'shared/monitoring/log'
|
||||
import { metrics } from 'shared/monitoring/metrics'
|
||||
import {convertPrivateUser, convertUser} from 'common/supabase/users'
|
||||
import {log, type Logger} from 'shared/monitoring/log'
|
||||
import {metrics} from 'shared/monitoring/metrics'
|
||||
|
||||
export { metrics }
|
||||
export { log, type Logger }
|
||||
@@ -62,8 +59,7 @@ export const isProd = () => {
|
||||
if (process.env.ENVIRONMENT) {
|
||||
return process.env.ENVIRONMENT == 'PROD'
|
||||
} 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 WebSocketServer, RawData, WebSocket } from 'ws'
|
||||
import { isError } from 'lodash'
|
||||
import { LOCAL_DEV, log, metrics } from 'shared/utils'
|
||||
import { log, metrics } from 'shared/utils'
|
||||
import { Switchboard } from './switchboard'
|
||||
import {
|
||||
BroadcastPayload,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ServerMessage,
|
||||
CLIENT_MESSAGE_SCHEMA,
|
||||
} from 'common/api/websockets'
|
||||
import {LOCAL_DEV} from "common/envs/constants";
|
||||
|
||||
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
|
||||
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$;
|
||||
|
||||
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
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
@@ -29,16 +29,16 @@ SELECT
|
||||
love_answers.free_response,
|
||||
love_answers.multiple_choice,
|
||||
love_answers.integer,
|
||||
lovers.age,
|
||||
lovers.gender,
|
||||
lovers.city,
|
||||
profiles.age,
|
||||
profiles.gender,
|
||||
profiles.city,
|
||||
users.data
|
||||
FROM
|
||||
lovers
|
||||
profiles
|
||||
JOIN
|
||||
love_answers ON lovers.user_id = love_answers.creator_id
|
||||
love_answers ON profiles.user_id = love_answers.creator_id
|
||||
join
|
||||
users on lovers.user_id = users.id
|
||||
users on profiles.user_id = users.id
|
||||
WHERE
|
||||
love_answers.question_id = p_question_id
|
||||
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 (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
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 (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
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 (
|
||||
created_time TIMESTAMPTZ DEFAULT now() 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 (
|
||||
answer_type TEXT DEFAULT 'free_response' 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 (
|
||||
created_time TIMESTAMPTZ DEFAULT now() 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 (
|
||||
created_time TIMESTAMPTZ DEFAULT now() 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 (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
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;
|
||||
\i backend/supabase/functions.sql
|
||||
\i backend/supabase/firebase.sql
|
||||
\i backend/supabase/lovers.sql
|
||||
\i backend/supabase/profiles.sql
|
||||
\i backend/supabase/users.sql
|
||||
\i backend/supabase/private_user_message_channels.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_seen_message_channels.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_likes.sql
|
||||
\i backend/supabase/love_questions.sql
|
||||
@@ -20,7 +20,5 @@ BEGIN;
|
||||
\i backend/supabase/user_notifications.sql
|
||||
\i backend/supabase/functions_others.sql
|
||||
\i backend/supabase/reports.sql
|
||||
\i backend/supabase/migrations/20250910_bio_to_jsonb.sql
|
||||
\i backend/supabase/migrations/20250910_add_bio_text.sql
|
||||
\i backend/supabase/migrations/20250910_pg_trgm.sql
|
||||
\i backend/supabase/bookmarked_searches.sql
|
||||
COMMIT;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
ALTER TABLE lovers ADD COLUMN bio_text tsvector;
|
||||
|
||||
CREATE OR REPLACE FUNCTION lovers_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 lovers_bio_tsvector_trigger
|
||||
BEFORE INSERT OR UPDATE OF bio ON lovers
|
||||
FOR EACH ROW EXECUTE FUNCTION lovers_bio_tsvector_update();
|
||||
|
||||
|
||||
create index on lovers using gin(bio_text);
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 1. Drop the old column
|
||||
alter table lovers drop column if exists bio;
|
||||
|
||||
-- 2. Add the new column as jsonb
|
||||
alter table lovers add column bio jsonb;
|
||||
@@ -1,4 +0,0 @@
|
||||
create extension if not exists pg_trgm;
|
||||
|
||||
CREATE INDEX lovers_bio_trgm_idx
|
||||
ON lovers USING gin ((bio::text) gin_trgm_ops);
|
||||
@@ -1,4 +1,3 @@
|
||||
-- This file is autogenerated from regen-schema.ts
|
||||
CREATE TABLE IF NOT EXISTS private_user_message_channel_members (
|
||||
channel_id BIGINT 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 (
|
||||
created_time TIMESTAMPTZ DEFAULT now() 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 (
|
||||
channel_id BIGINT 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 (
|
||||
channel_id BIGINT 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 (
|
||||
data JSONB NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
-- This file is autogenerated from regen-schema.ts
|
||||
CREATE TABLE IF NOT EXISTS lover_comments (
|
||||
CREATE TABLE IF NOT EXISTS profile_comments (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
content JSONB NOT NULL,
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
@@ -13,12 +12,12 @@ CREATE TABLE IF NOT EXISTS lover_comments (
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE lover_comments ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE profile_comments ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies
|
||||
DROP POLICY IF EXISTS "public read" ON lover_comments;
|
||||
CREATE POLICY "public read" ON lover_comments FOR ALL USING (true);
|
||||
DROP POLICY IF EXISTS "public read" ON profile_comments;
|
||||
CREATE POLICY "public read" ON profile_comments FOR ALL USING (true);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS lover_comments_user_id_idx
|
||||
ON public.lover_comments USING btree (on_user_id);
|
||||
CREATE INDEX IF NOT EXISTS profile_comments_user_id_idx
|
||||
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
|
||||
reports (
|
||||
content_id text not null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
-- This file is autogenerated from regen-schema.ts
|
||||
create table if not exists
|
||||
temp_users (
|
||||
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 (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
ad_id TEXT,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
-- This file is autogenerated from regen-schema.ts
|
||||
CREATE TABLE IF NOT EXISTS user_notifications (
|
||||
notification_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 (
|
||||
created_time TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
|
||||
@@ -1 +1 @@
|
||||
-- This file is autogenerated from regen-schema.ts
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
contentSchema,
|
||||
combinedLoveUsersSchema,
|
||||
baseLoversSchema,
|
||||
baseProfilesSchema,
|
||||
arraybeSchema,
|
||||
} from 'common/api/zod-types'
|
||||
import { PrivateChatMessage } from 'common/chat-message'
|
||||
import { CompatibilityScore } from 'common/love/compatibility-score'
|
||||
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 { PrivateUser, User } from 'common/user'
|
||||
import { z } from 'zod'
|
||||
@@ -88,11 +88,11 @@ export const API = (_apiTypeCheck = {
|
||||
})
|
||||
.strict(),
|
||||
},
|
||||
'create-lover': {
|
||||
'create-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
returns: {} as Row<'lovers'>,
|
||||
props: baseLoversSchema,
|
||||
returns: {} as Row<'profiles'>,
|
||||
props: baseProfilesSchema,
|
||||
},
|
||||
report: {
|
||||
method: 'POST',
|
||||
@@ -143,11 +143,11 @@ export const API = (_apiTypeCheck = {
|
||||
}),
|
||||
returns: {} as FullUser,
|
||||
},
|
||||
'update-lover': {
|
||||
'update-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
props: combinedLoveUsersSchema.partial(),
|
||||
returns: {} as LoverRow,
|
||||
returns: {} as ProfileRow,
|
||||
},
|
||||
'update-notif-settings': {
|
||||
method: 'POST',
|
||||
@@ -212,14 +212,14 @@ export const API = (_apiTypeCheck = {
|
||||
})
|
||||
.strict(),
|
||||
},
|
||||
'compatible-lovers': {
|
||||
'compatible-profiles': {
|
||||
method: 'GET',
|
||||
authed: false,
|
||||
props: z.object({ userId: z.string() }),
|
||||
returns: {} as {
|
||||
lover: Lover
|
||||
compatibleLovers: Lover[]
|
||||
loverCompatibilityScores: {
|
||||
profile: Profile
|
||||
compatibleProfiles: Profile[]
|
||||
profileCompatibilityScores: {
|
||||
[userId: string]: CompatibilityScore
|
||||
}
|
||||
},
|
||||
@@ -246,7 +246,7 @@ export const API = (_apiTypeCheck = {
|
||||
})[]
|
||||
},
|
||||
},
|
||||
'like-lover': {
|
||||
'like-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
props: z.object({
|
||||
@@ -257,7 +257,7 @@ export const API = (_apiTypeCheck = {
|
||||
status: 'success'
|
||||
},
|
||||
},
|
||||
'ship-lovers': {
|
||||
'ship-profiles': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
props: z.object({
|
||||
@@ -293,7 +293,7 @@ export const API = (_apiTypeCheck = {
|
||||
hasFreeLike: boolean
|
||||
},
|
||||
},
|
||||
'star-lover': {
|
||||
'star-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
props: z.object({
|
||||
@@ -304,7 +304,7 @@ export const API = (_apiTypeCheck = {
|
||||
status: 'success'
|
||||
},
|
||||
},
|
||||
'get-lovers': {
|
||||
'get-profiles': {
|
||||
method: 'GET',
|
||||
authed: false,
|
||||
props: z
|
||||
@@ -331,10 +331,10 @@ export const API = (_apiTypeCheck = {
|
||||
.strict(),
|
||||
returns: {} as {
|
||||
status: 'success' | 'fail'
|
||||
lovers: Lover[]
|
||||
profiles: Profile[]
|
||||
},
|
||||
},
|
||||
'get-lover-answers': {
|
||||
'get-profile-answers': {
|
||||
method: 'GET',
|
||||
authed: false,
|
||||
props: z.object({ userId: z.string() }).strict(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { type APIPath } from './schema'
|
||||
|
||||
type ErrorCode =
|
||||
| 400 // your input is bad (like zod is mad)
|
||||
@@ -20,8 +19,8 @@ export class APIError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function pathWithPrefix(path: APIPath) {
|
||||
return `v0/${path}`
|
||||
export function pathWithPrefix(path: string) {
|
||||
return `/v0${path}`
|
||||
}
|
||||
|
||||
export function getWebsocketUrl() {
|
||||
|
||||
@@ -42,13 +42,13 @@ const genderType = z.string()
|
||||
// ])
|
||||
const genderTypes = z.array(genderType)
|
||||
|
||||
export const baseLoversSchema = z.object({
|
||||
export const baseProfilesSchema = z.object({
|
||||
// Required fields
|
||||
age: z.number().min(18).max(100),
|
||||
age: z.number().min(18).max(100).optional(),
|
||||
gender: genderType,
|
||||
pref_gender: genderTypes,
|
||||
pref_age_min: z.number().min(18).max(999),
|
||||
pref_age_max: z.number().min(18).max(1000),
|
||||
pref_age_min: z.number().min(18).max(100).optional(),
|
||||
pref_age_max: z.number().min(18).max(100).optional(),
|
||||
pref_relation_styles: z.array(
|
||||
z.union([
|
||||
z.literal('collaboration'),
|
||||
@@ -74,7 +74,7 @@ export const baseLoversSchema = z.object({
|
||||
referred_by_username: z.string().optional(),
|
||||
})
|
||||
|
||||
const optionalLoversSchema = z.object({
|
||||
const optionalProfilesSchema = z.object({
|
||||
political_beliefs: z.array(z.string()).optional(),
|
||||
religious_belief_strength: z.number().optional(),
|
||||
religious_beliefs: z.string().optional(),
|
||||
@@ -100,4 +100,4 @@ const optionalLoversSchema = z.object({
|
||||
})
|
||||
|
||||
export const combinedLoveUsersSchema =
|
||||
baseLoversSchema.merge(optionalLoversSchema)
|
||||
baseProfilesSchema.merge(optionalProfilesSchema)
|
||||
|
||||
@@ -11,8 +11,8 @@ export type Comment = {
|
||||
replyToCommentId?: string
|
||||
userId: string
|
||||
|
||||
// lover
|
||||
commentType: 'lover'
|
||||
// profile
|
||||
commentType: 'profile'
|
||||
onUserId: string
|
||||
|
||||
/** @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 { EnvConfig, PROD_CONFIG } from './prod'
|
||||
import {DEV_CONFIG} from './dev'
|
||||
import {EnvConfig, PROD_CONFIG} from './prod'
|
||||
|
||||
// Valid in web client & Vercel deployments only.
|
||||
export const ENV = (process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD') as
|
||||
@@ -107,3 +107,6 @@ export const RESERVED_PATHS = [
|
||||
'web',
|
||||
'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 = {
|
||||
...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 = {
|
||||
posthogKey: 'phc_xT16KyBj7GsWnAwifoH4HiWKTFhuohRrfy3t5DK6ZIv',
|
||||
posthogKey: 'phc_tFvQzHiMVdaAIgE38xqYomMN8q8SB5K45fqmkKNjfBU',
|
||||
domain: 'compassmeet.com',
|
||||
firebaseConfig: {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || '',
|
||||
|
||||
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 }
|
||||
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 { LoverRow } from 'common/love/lover'
|
||||
import { ProfileRow } from 'common/love/profile'
|
||||
import { Row as rowFor } from 'common/supabase/utils'
|
||||
import {
|
||||
areAgeCompatible,
|
||||
@@ -132,14 +132,14 @@ export function getScoredAnswerCompatibility(
|
||||
)
|
||||
}
|
||||
|
||||
export const getLoversCompatibilityFactor = (
|
||||
lover1: LoverRow,
|
||||
lover2: LoverRow
|
||||
export const getProfilesCompatibilityFactor = (
|
||||
profile1: ProfileRow,
|
||||
profile2: ProfileRow
|
||||
) => {
|
||||
let multiplier = 1
|
||||
multiplier *= areAgeCompatible(lover1, lover2) ? 1 : 0.5
|
||||
multiplier *= areRelationshipStyleCompatible(lover1, lover2) ? 1 : 0.5
|
||||
multiplier *= areWantKidsCompatible(lover1, lover2) ? 1 : 0.5
|
||||
multiplier *= areLocationCompatible(lover1, lover2) ? 1 : 0.1
|
||||
multiplier *= areAgeCompatible(profile1, profile2) ? 1 : 0.5
|
||||
multiplier *= areRelationshipStyleCompatible(profile1, profile2) ? 1 : 0.5
|
||||
multiplier *= areWantKidsCompatible(profile1, profile2) ? 1 : 0.5
|
||||
multiplier *= areLocationCompatible(profile1, profile2) ? 1 : 0.1
|
||||
return multiplier
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LoverRow } from 'common/love/lover'
|
||||
import { ProfileRow } from 'common/love/profile'
|
||||
import {MAX_INT, MIN_INT} from "common/constants";
|
||||
|
||||
const isPreferredGender = (
|
||||
preferredGenders: string[] | undefined,
|
||||
@@ -17,53 +18,53 @@ const isPreferredGender = (
|
||||
return preferredGenders.includes(gender) || gender === 'non-binary'
|
||||
}
|
||||
|
||||
export const areGenderCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
||||
// console.log('areGenderCompatible', isPreferredGender(lover1.pref_gender, lover2.gender), isPreferredGender(lover2.pref_gender, lover1.gender))
|
||||
export const areGenderCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||
// console.log('areGenderCompatible', isPreferredGender(profile1.pref_gender, profile2.gender), isPreferredGender(profile2.pref_gender, profile1.gender))
|
||||
return (
|
||||
isPreferredGender(lover1.pref_gender, lover2.gender) &&
|
||||
isPreferredGender(lover2.pref_gender, lover1.gender)
|
||||
isPreferredGender(profile1.pref_gender, profile2.gender) &&
|
||||
isPreferredGender(profile2.pref_gender, profile1.gender)
|
||||
)
|
||||
}
|
||||
|
||||
const satisfiesAgeRange = (lover: LoverRow, age: number) => {
|
||||
return age >= lover.pref_age_min && age <= lover.pref_age_max
|
||||
const satisfiesAgeRange = (profile: ProfileRow, age: number | null | undefined) => {
|
||||
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 (
|
||||
satisfiesAgeRange(lover1, lover2.age) &&
|
||||
satisfiesAgeRange(lover2, lover1.age)
|
||||
satisfiesAgeRange(profile1, profile2.age) &&
|
||||
satisfiesAgeRange(profile2, profile1.age)
|
||||
)
|
||||
}
|
||||
|
||||
export const areLocationCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
||||
export const areLocationCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||
if (
|
||||
!lover1.city_latitude ||
|
||||
!lover2.city_latitude ||
|
||||
!lover1.city_longitude ||
|
||||
!lover2.city_longitude
|
||||
!profile1.city_latitude ||
|
||||
!profile2.city_latitude ||
|
||||
!profile1.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 longigudeDiff = Math.abs(lover1.city_longitude - lover2.city_longitude)
|
||||
const latitudeDiff = Math.abs(profile1.city_latitude - profile2.city_latitude)
|
||||
const longigudeDiff = Math.abs(profile1.city_longitude - profile2.city_longitude)
|
||||
|
||||
const root = (latitudeDiff ** 2 + longigudeDiff ** 2) ** 0.5
|
||||
return root < 2.5
|
||||
}
|
||||
|
||||
export const areRelationshipStyleCompatible = (
|
||||
lover1: LoverRow,
|
||||
lover2: LoverRow
|
||||
profile1: ProfileRow,
|
||||
profile2: ProfileRow
|
||||
) => {
|
||||
return lover1.pref_relation_styles.some((style) =>
|
||||
lover2.pref_relation_styles.includes(style)
|
||||
return profile1.pref_relation_styles.some((style) =>
|
||||
profile2.pref_relation_styles.includes(style)
|
||||
)
|
||||
}
|
||||
|
||||
export const areWantKidsCompatible = (lover1: LoverRow, lover2: LoverRow) => {
|
||||
const { wants_kids_strength: kids1 } = lover1
|
||||
const { wants_kids_strength: kids2 } = lover2
|
||||
export const areWantKidsCompatible = (profile1: ProfileRow, profile2: ProfileRow) => {
|
||||
const { wants_kids_strength: kids1 } = profile1
|
||||
const { wants_kids_strength: kids2 } = profile2
|
||||
|
||||
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 { LoverRow } from 'common/love/lover'
|
||||
import { ProfileRow } from 'common/love/profile'
|
||||
import { buildOgUrl } from 'common/util/og'
|
||||
|
||||
// TODO: handle age, gender undefined better
|
||||
@@ -8,21 +8,21 @@ export type LoveOgProps = {
|
||||
avatarUrl: string
|
||||
username: string
|
||||
name: string
|
||||
// lover props
|
||||
// profile props
|
||||
age: string
|
||||
city: string
|
||||
gender: string
|
||||
}
|
||||
|
||||
export function getLoveOgImageUrl(user: User, lover?: LoverRow | null) {
|
||||
export function getLoveOgImageUrl(user: User, profile?: ProfileRow | null) {
|
||||
const loveProps = {
|
||||
avatarUrl: lover?.pinned_url,
|
||||
avatarUrl: profile?.pinned_url,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
age: lover?.age.toString() ?? '25',
|
||||
city: lover?.city ?? 'Internet',
|
||||
gender: lover?.gender ?? '???',
|
||||
age: profile?.age?.toString() ?? '25',
|
||||
city: profile?.city ?? 'Internet',
|
||||
gender: profile?.gender ?? '???',
|
||||
} 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 = [
|
||||
'new_match', // new match markets
|
||||
'comment_on_lover', // endorsements
|
||||
'comment_on_profile', // endorsements
|
||||
'love_like',
|
||||
'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
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export const secrets = (
|
||||
'TEST_CREATE_USER_KEY',
|
||||
'GEODB_API_KEY',
|
||||
'RESEND_KEY',
|
||||
'COMPASS_API_KEY',
|
||||
'NEXT_PUBLIC_FIREBASE_API_KEY',
|
||||
// Some typescript voodoo to keep the string literal types while being not readonly.
|
||||
] as const
|
||||
|
||||
@@ -42,8 +42,6 @@ const stripper: { [key in Site]: (input: string) => string } = {
|
||||
s
|
||||
.replace(/^(https?:\/\/)?(www\.)?(\w+\.)?substack\.com\//, '')
|
||||
.replace(/\/$/, ''),
|
||||
// onlyfans: (s) =>
|
||||
// s.replace(/^(https?:\/\/)?(www\.)?onlyfans\.com\//, '').replace(/\/$/, ''),
|
||||
instagram: (s) =>
|
||||
s
|
||||
.replace(/^(https?:\/\/)?(www\.)?instagram\.com\//, '')
|
||||
@@ -80,13 +78,12 @@ const urler: { [key in Site]: (handle: string) => string } = {
|
||||
mastodon: (s) =>
|
||||
s.includes('@') ? `https://${s.split('@')[1]}/@${s.split('@')[0]}` : s,
|
||||
substack: (s) => `https://${s}.substack.com`,
|
||||
// onlyfans: (s) => `https://onlyfans.com/${s}`,
|
||||
instagram: (s) => `https://instagram.com/${s}`,
|
||||
github: (s) => `https://github.com/${s}`,
|
||||
linkedin: (s) => `https://linkedin.com/in/${s}`,
|
||||
facebook: (s) => `https://facebook.com/${s}`,
|
||||
spotify: (s) => `https://open.spotify.com/user/${s}`,
|
||||
paypal: (s) => `https://paypal.com/user/${s}`,
|
||||
paypal: (s) => `https://paypal.com/paypalme/${s}`,
|
||||
patreon: (s) => `https://patreon.com/user/${s}`,
|
||||
}
|
||||
|
||||
@@ -97,7 +94,6 @@ export const PLATFORM_LABELS: { [key in Site]: string } = {
|
||||
bluesky: 'Bluesky',
|
||||
mastodon: 'Mastodon',
|
||||
substack: 'Substack',
|
||||
// onlyfans: 'OnlyFans',
|
||||
instagram: 'Instagram',
|
||||
github: 'GitHub',
|
||||
linkedin: 'LinkedIn',
|
||||
|
||||
@@ -2,10 +2,10 @@ import { type JSONContent } from '@tiptap/core'
|
||||
import { type Row, tsToMillis } from './utils'
|
||||
import { type Comment } from 'common/comment'
|
||||
|
||||
export const convertComment = (row: Row<'lover_comments'>): Comment => ({
|
||||
export const convertComment = (row: Row<'profile_comments'>): Comment => ({
|
||||
id: row.id + '',
|
||||
userId: row.user_id,
|
||||
commentType: 'lover',
|
||||
commentType: 'profile',
|
||||
onUserId: row.on_user_id,
|
||||
createdTime: tsToMillis(row.created_time),
|
||||
userName: row.user_name,
|
||||
|
||||
@@ -7,8 +7,43 @@ export type Json =
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: '13.0.4'
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
bookmarked_searches: {
|
||||
Row: {
|
||||
created_time: string
|
||||
creator_id: string
|
||||
id: number
|
||||
last_notified_at: string | null
|
||||
location: Json | null
|
||||
search_filters: Json | null
|
||||
search_name: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_time?: string
|
||||
creator_id: string
|
||||
id?: never
|
||||
last_notified_at?: string | null
|
||||
location?: Json | null
|
||||
search_filters?: Json | null
|
||||
search_name?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_time?: string
|
||||
creator_id?: string
|
||||
id?: never
|
||||
last_notified_at?: string | null
|
||||
location?: Json | null
|
||||
search_filters?: Json | null
|
||||
search_name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
love_answers: {
|
||||
Row: {
|
||||
created_time: string
|
||||
@@ -186,7 +221,7 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
lover_comments: {
|
||||
profile_comments: {
|
||||
Row: {
|
||||
content: Json
|
||||
created_time: string
|
||||
@@ -225,141 +260,6 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
lovers: {
|
||||
Row: {
|
||||
age: number
|
||||
bio: Json | null
|
||||
born_in_location: string | null
|
||||
city: string
|
||||
city_latitude: number | null
|
||||
city_longitude: number | null
|
||||
comments_enabled: boolean
|
||||
company: string | null
|
||||
country: string | null
|
||||
created_time: string
|
||||
drinks_per_month: number | null
|
||||
education_level: string | null
|
||||
ethnicity: string[] | null
|
||||
gender: string
|
||||
geodb_city_id: string | null
|
||||
has_kids: number | null
|
||||
height_in_inches: number | null
|
||||
id: number
|
||||
is_smoker: boolean | null
|
||||
is_vegetarian_or_vegan: boolean | null
|
||||
last_online_time: string
|
||||
looking_for_matches: boolean
|
||||
messaging_status: string
|
||||
occupation: string | null
|
||||
occupation_title: string | null
|
||||
photo_urls: string[] | null
|
||||
pinned_url: string | null
|
||||
political_beliefs: string[] | null
|
||||
pref_age_max: number
|
||||
pref_age_min: number
|
||||
pref_gender: string[]
|
||||
pref_relation_styles: string[]
|
||||
referred_by_username: string | null
|
||||
region_code: string | null
|
||||
religious_belief_strength: number | null
|
||||
religious_beliefs: string | null
|
||||
twitter: string | null
|
||||
university: string | null
|
||||
user_id: string
|
||||
visibility: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength: number
|
||||
website: string | null
|
||||
}
|
||||
Insert: {
|
||||
age?: number
|
||||
bio?: Json | null
|
||||
born_in_location?: string | null
|
||||
city: string
|
||||
city_latitude?: number | null
|
||||
city_longitude?: number | null
|
||||
comments_enabled?: boolean
|
||||
company?: string | null
|
||||
country?: string | null
|
||||
created_time?: string
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
gender: string
|
||||
geodb_city_id?: string | null
|
||||
has_kids?: number | null
|
||||
height_in_inches?: number | null
|
||||
id?: never
|
||||
is_smoker?: boolean | null
|
||||
is_vegetarian_or_vegan?: boolean | null
|
||||
last_online_time?: string
|
||||
looking_for_matches?: boolean
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
pinned_url?: string | null
|
||||
political_beliefs?: string[] | null
|
||||
pref_age_max?: number
|
||||
pref_age_min?: number
|
||||
pref_gender: string[]
|
||||
pref_relation_styles: string[]
|
||||
referred_by_username?: string | null
|
||||
region_code?: string | null
|
||||
religious_belief_strength?: number | null
|
||||
religious_beliefs?: string | null
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id: string
|
||||
visibility?: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength?: number
|
||||
website?: string | null
|
||||
}
|
||||
Update: {
|
||||
age?: number
|
||||
bio?: Json | null
|
||||
born_in_location?: string | null
|
||||
city?: string
|
||||
city_latitude?: number | null
|
||||
city_longitude?: number | null
|
||||
comments_enabled?: boolean
|
||||
company?: string | null
|
||||
country?: string | null
|
||||
created_time?: string
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
gender?: string
|
||||
geodb_city_id?: string | null
|
||||
has_kids?: number | null
|
||||
height_in_inches?: number | null
|
||||
id?: never
|
||||
is_smoker?: boolean | null
|
||||
is_vegetarian_or_vegan?: boolean | null
|
||||
last_online_time?: string
|
||||
looking_for_matches?: boolean
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
pinned_url?: string | null
|
||||
political_beliefs?: string[] | null
|
||||
pref_age_max?: number
|
||||
pref_age_min?: number
|
||||
pref_gender?: string[]
|
||||
pref_relation_styles?: string[]
|
||||
referred_by_username?: string | null
|
||||
region_code?: string | null
|
||||
religious_belief_strength?: number | null
|
||||
religious_beliefs?: string | null
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id?: string
|
||||
visibility?: Database['public']['Enums']['lover_visibility']
|
||||
wants_kids_strength?: number
|
||||
website?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
private_user_message_channel_members: {
|
||||
Row: {
|
||||
channel_id: number
|
||||
@@ -407,13 +307,13 @@ export type Database = {
|
||||
}
|
||||
Insert: {
|
||||
created_time?: string
|
||||
id?: never
|
||||
id?: number
|
||||
last_updated_time?: string
|
||||
title?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_time?: string
|
||||
id?: never
|
||||
id?: number
|
||||
last_updated_time?: string
|
||||
title?: string | null
|
||||
}
|
||||
@@ -501,6 +401,144 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
age: number | null
|
||||
bio: Json | null
|
||||
born_in_location: string | null
|
||||
city: string
|
||||
city_latitude: number | null
|
||||
city_longitude: number | null
|
||||
comments_enabled: boolean
|
||||
company: string | null
|
||||
country: string | null
|
||||
created_time: string
|
||||
drinks_per_month: number | null
|
||||
education_level: string | null
|
||||
ethnicity: string[] | null
|
||||
gender: string
|
||||
geodb_city_id: string | null
|
||||
has_kids: number | null
|
||||
height_in_inches: number | null
|
||||
id: number
|
||||
is_smoker: boolean | null
|
||||
is_vegetarian_or_vegan: boolean | null
|
||||
last_modification_time: string
|
||||
last_online_time: string
|
||||
looking_for_matches: boolean
|
||||
messaging_status: string
|
||||
occupation: string | null
|
||||
occupation_title: string | null
|
||||
photo_urls: string[] | null
|
||||
pinned_url: string | null
|
||||
political_beliefs: string[] | null
|
||||
pref_age_max: number | null
|
||||
pref_age_min: number | null
|
||||
pref_gender: string[]
|
||||
pref_relation_styles: string[]
|
||||
referred_by_username: string | null
|
||||
region_code: string | null
|
||||
religious_belief_strength: number | null
|
||||
religious_beliefs: string | null
|
||||
twitter: string | null
|
||||
university: string | null
|
||||
user_id: string
|
||||
visibility: Database['public']['Enums']['profile_visibility']
|
||||
wants_kids_strength: number
|
||||
website: string | null
|
||||
}
|
||||
Insert: {
|
||||
age?: number | null
|
||||
bio?: Json | null
|
||||
born_in_location?: string | null
|
||||
city: string
|
||||
city_latitude?: number | null
|
||||
city_longitude?: number | null
|
||||
comments_enabled?: boolean
|
||||
company?: string | null
|
||||
country?: string | null
|
||||
created_time?: string
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
gender: string
|
||||
geodb_city_id?: string | null
|
||||
has_kids?: number | null
|
||||
height_in_inches?: number | null
|
||||
id?: never
|
||||
is_smoker?: boolean | null
|
||||
is_vegetarian_or_vegan?: boolean | null
|
||||
last_modification_time?: string
|
||||
last_online_time?: string
|
||||
looking_for_matches?: boolean
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
pinned_url?: string | null
|
||||
political_beliefs?: string[] | null
|
||||
pref_age_max?: number | null
|
||||
pref_age_min?: number | null
|
||||
pref_gender: string[]
|
||||
pref_relation_styles: string[]
|
||||
referred_by_username?: string | null
|
||||
region_code?: string | null
|
||||
religious_belief_strength?: number | null
|
||||
religious_beliefs?: string | null
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id: string
|
||||
visibility?: Database['public']['Enums']['profile_visibility']
|
||||
wants_kids_strength?: number
|
||||
website?: string | null
|
||||
}
|
||||
Update: {
|
||||
age?: number | null
|
||||
bio?: Json | null
|
||||
born_in_location?: string | null
|
||||
city?: string
|
||||
city_latitude?: number | null
|
||||
city_longitude?: number | null
|
||||
comments_enabled?: boolean
|
||||
company?: string | null
|
||||
country?: string | null
|
||||
created_time?: string
|
||||
drinks_per_month?: number | null
|
||||
education_level?: string | null
|
||||
ethnicity?: string[] | null
|
||||
gender?: string
|
||||
geodb_city_id?: string | null
|
||||
has_kids?: number | null
|
||||
height_in_inches?: number | null
|
||||
id?: never
|
||||
is_smoker?: boolean | null
|
||||
is_vegetarian_or_vegan?: boolean | null
|
||||
last_modification_time?: string
|
||||
last_online_time?: string
|
||||
looking_for_matches?: boolean
|
||||
messaging_status?: string
|
||||
occupation?: string | null
|
||||
occupation_title?: string | null
|
||||
photo_urls?: string[] | null
|
||||
pinned_url?: string | null
|
||||
political_beliefs?: string[] | null
|
||||
pref_age_max?: number | null
|
||||
pref_age_min?: number | null
|
||||
pref_gender?: string[]
|
||||
pref_relation_styles?: string[]
|
||||
referred_by_username?: string | null
|
||||
region_code?: string | null
|
||||
religious_belief_strength?: number | null
|
||||
religious_beliefs?: string | null
|
||||
twitter?: string | null
|
||||
university?: string | null
|
||||
user_id?: string
|
||||
visibility?: Database['public']['Enums']['profile_visibility']
|
||||
wants_kids_strength?: number
|
||||
website?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
reports: {
|
||||
Row: {
|
||||
content_id: string
|
||||
@@ -552,33 +590,6 @@ export type Database = {
|
||||
}
|
||||
]
|
||||
}
|
||||
temp_users: {
|
||||
Row: {
|
||||
created_time: string | null
|
||||
id: string | null
|
||||
name: string | null
|
||||
private_user_data: Json | null
|
||||
user_data: Json | null
|
||||
username: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_time?: string | null
|
||||
id?: string | null
|
||||
name?: string | null
|
||||
private_user_data?: Json | null
|
||||
user_data?: Json | null
|
||||
username?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_time?: string | null
|
||||
id?: string | null
|
||||
name?: string | null
|
||||
private_user_data?: Json | null
|
||||
user_data?: Json | null
|
||||
username?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
user_events: {
|
||||
Row: {
|
||||
ad_id: string | null
|
||||
@@ -663,7 +674,7 @@ export type Database = {
|
||||
}
|
||||
Functions: {
|
||||
calculate_earth_distance_km: {
|
||||
Args: { lat1: number; lon1: number; lat2: number; lon2: number }
|
||||
Args: { lat1: number; lat2: number; lon1: number; lon2: number }
|
||||
Returns: number
|
||||
}
|
||||
can_access_private_messages: {
|
||||
@@ -682,16 +693,36 @@ export type Database = {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: Record<string, unknown>[]
|
||||
}
|
||||
get_love_question_answers_and_lovers: {
|
||||
get_love_question_answers_and_profiles: {
|
||||
Args: { p_question_id: number }
|
||||
Returns: Record<string, unknown>[]
|
||||
}
|
||||
gtrgm_compress: {
|
||||
Args: { '': unknown }
|
||||
Returns: unknown
|
||||
}
|
||||
gtrgm_decompress: {
|
||||
Args: { '': unknown }
|
||||
Returns: unknown
|
||||
}
|
||||
gtrgm_in: {
|
||||
Args: { '': unknown }
|
||||
Returns: unknown
|
||||
}
|
||||
gtrgm_options: {
|
||||
Args: { '': unknown }
|
||||
Returns: undefined
|
||||
}
|
||||
gtrgm_out: {
|
||||
Args: { '': unknown }
|
||||
Returns: unknown
|
||||
}
|
||||
is_admin: {
|
||||
Args: Record<PropertyKey, never> | { user_id: string }
|
||||
Returns: boolean
|
||||
}
|
||||
millis_interval: {
|
||||
Args: { start_millis: number; end_millis: number }
|
||||
Args: { end_millis: number; start_millis: number }
|
||||
Returns: unknown
|
||||
}
|
||||
millis_to_ts: {
|
||||
@@ -702,17 +733,29 @@ export type Database = {
|
||||
Args: { length: number }
|
||||
Returns: string
|
||||
}
|
||||
set_limit: {
|
||||
Args: { '': number }
|
||||
Returns: number
|
||||
}
|
||||
show_limit: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: number
|
||||
}
|
||||
show_trgm: {
|
||||
Args: { '': string }
|
||||
Returns: string[]
|
||||
}
|
||||
to_jsonb: {
|
||||
Args: { '': Json }
|
||||
Returns: Json
|
||||
}
|
||||
ts_to_millis: {
|
||||
Args: { ts: string } | { ts: string }
|
||||
Args: { ts: string }
|
||||
Returns: number
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
lover_visibility: 'public' | 'member'
|
||||
profile_visibility: 'public' | 'member'
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
@@ -720,21 +763,25 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, 'public'>]
|
||||
type DatabaseWithoutInternals = Omit<Database, '__InternalSupabase'>
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, 'public'>]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema['Tables'] & DefaultSchema['Views'])
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
Database[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Views'])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
@@ -752,14 +799,16 @@ export type Tables<
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema['Tables']
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
@@ -775,14 +824,16 @@ export type TablesInsert<
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema['Tables']
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables']
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions['schema']]['Tables'][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
@@ -798,14 +849,16 @@ export type TablesUpdate<
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema['Enums']
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums']
|
||||
: never = never
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions['schema']]['Enums'][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema['Enums']
|
||||
? DefaultSchema['Enums'][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
@@ -813,14 +866,16 @@ export type Enums<
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema['CompositeTypes']
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes']
|
||||
: never = never
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema['CompositeTypes']
|
||||
? DefaultSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
@@ -828,7 +883,7 @@ export type CompositeTypes<
|
||||
export const Constants = {
|
||||
public: {
|
||||
Enums: {
|
||||
lover_visibility: ['public', 'member'],
|
||||
profile_visibility: ['public', 'member'],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -8,6 +8,7 @@ export type notification_preferences = {
|
||||
new_endorsement: notification_destination_types[]
|
||||
new_love_like: notification_destination_types[]
|
||||
new_love_ship: notification_destination_types[]
|
||||
new_search_alerts: notification_destination_types[]
|
||||
|
||||
// User-related
|
||||
new_message: notification_destination_types[]
|
||||
@@ -37,6 +38,7 @@ export const getDefaultNotificationPreferences = (isDev?: boolean) => {
|
||||
}
|
||||
const defaults: notification_preferences = {
|
||||
new_match: constructPref(true, true, true),
|
||||
new_search_alerts: constructPref(true, true, true),
|
||||
new_endorsement: constructPref(true, true, true),
|
||||
new_love_like: constructPref(true, false, false),
|
||||
new_love_ship: constructPref(true, false, false),
|
||||
@@ -59,8 +61,10 @@ export const getNotificationDestinationsForUser = (
|
||||
privateUser: PrivateUser,
|
||||
type: notification_preference
|
||||
) => {
|
||||
const destinations = privateUser.notificationPreferences[type]
|
||||
const opt_out = privateUser.notificationPreferences.opt_out_all
|
||||
let destinations = privateUser.notificationPreferences[type]
|
||||
if (!destinations) destinations = ['email', 'browser', 'mobile']
|
||||
let opt_out = privateUser.notificationPreferences.opt_out_all
|
||||
if (!opt_out) opt_out = []
|
||||
|
||||
return {
|
||||
sendToEmail: destinations.includes('email') && !opt_out.includes('email'),
|
||||
|
||||
@@ -67,6 +67,7 @@ export async function baseApiCall(props: {
|
||||
body:
|
||||
params == null || method === 'GET' ? undefined : JSON.stringify(params),
|
||||
})
|
||||
// console.log(req)
|
||||
return fetch(req).then(async (resp) => {
|
||||
const json = (await resp.json()) as { [k: string]: any }
|
||||
if (!resp.ok) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DOMAIN } from 'common/envs/constants'
|
||||
import {DOMAIN, LOCAL_DEV, LOCAL_WEB_URL} from 'common/envs/constants'
|
||||
|
||||
// opengraph functions that run in static props or client-side, but not in the edge (in image creation)
|
||||
|
||||
@@ -10,10 +10,5 @@ export function buildOgUrl<P extends Record<string, string>>(
|
||||
const generateUrlParams = (params: P) =>
|
||||
new URLSearchParams(params).toString()
|
||||
|
||||
// Change to localhost:3000 for local testing
|
||||
const url =
|
||||
// `http://localhost:3000/api/og/${endpoint}?` + generateUrlParams(props)
|
||||
`https://${domain ?? DOMAIN}/api/og/${endpoint}?` + generateUrlParams(props)
|
||||
|
||||
return url
|
||||
return `https://${domain ?? LOCAL_DEV ? LOCAL_WEB_URL : DOMAIN}/api/og/${endpoint}?` + generateUrlParams(props)
|
||||
}
|
||||
|
||||
68
common/src/wants-kids.ts
Normal file
68
common/src/wants-kids.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {hasKidsLabels} from "common/has-kids";
|
||||
|
||||
export type KidLabel = {
|
||||
name: string
|
||||
shortName: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
export type KidsLabelsMap = Record<string, KidLabel>
|
||||
|
||||
export const wantsKidsLabels: KidsLabelsMap = {
|
||||
no_preference: {
|
||||
name: 'Any preference',
|
||||
shortName: 'Either',
|
||||
strength: -1,
|
||||
},
|
||||
wants_kids: {
|
||||
name: 'Wants kids',
|
||||
shortName: 'Yes',
|
||||
strength: 2,
|
||||
},
|
||||
doesnt_want_kids: {
|
||||
name: `Doesn't want kids`,
|
||||
shortName: 'No',
|
||||
strength: 0,
|
||||
},
|
||||
}
|
||||
export const wantsKidsNames = Object.values(wantsKidsLabels).reduce<Record<number, string>>(
|
||||
(acc, {strength, name}) => {
|
||||
acc[strength] = name
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
export type wantsKidsDatabase = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
export function wantsKidsToHasKidsFilter(wantsKidsStrength: wantsKidsDatabase) {
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return hasKidsLabels.doesnt_have_kids.value
|
||||
}
|
||||
return hasKidsLabels.no_preference.value
|
||||
}
|
||||
|
||||
export function wantsKidsDatabaseToWantsKidsFilter(
|
||||
wantsKidsStrength: wantsKidsDatabase
|
||||
) {
|
||||
// console.log(wantsKidsStrength)
|
||||
if (wantsKidsStrength == wantsKidsLabels.no_preference.strength) {
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
if (wantsKidsStrength > wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.wants_kids.strength
|
||||
}
|
||||
if (wantsKidsStrength < wantsKidsLabels.wants_kids.strength) {
|
||||
return wantsKidsLabels.doesnt_want_kids.strength
|
||||
}
|
||||
return wantsKidsLabels.no_preference.strength
|
||||
}
|
||||
|
||||
export const generateChoicesMap = (labels: KidsLabelsMap): Record<string, number> => {
|
||||
return Object.values(labels).reduce(
|
||||
(acc: Record<string, number>, label: KidLabel) => {
|
||||
acc[label.shortName] = label.strength
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user