mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-24 17:41:27 -04:00
Massive Next.js (14->16) and React (18->19) upgrade
This commit is contained in:
@@ -528,7 +528,6 @@ How we apply it here
|
||||
This project uses three complementary test types. Use the right level for the job:
|
||||
|
||||
- Unit tests
|
||||
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
@@ -537,7 +536,6 @@ This project uses three complementary test types. Use the right level for the jo
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
|
||||
- Integration tests
|
||||
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -30,4 +30,4 @@ runs:
|
||||
- name: Post-install
|
||||
if: steps.cache-node-modules.outputs.cache-hit == 'true'
|
||||
run: yarn postinstall
|
||||
shell: bash
|
||||
shell: bash
|
||||
|
||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -522,7 +522,6 @@ How we apply it here
|
||||
This project uses three complementary test types. Use the right level for the job:
|
||||
|
||||
- Unit tests
|
||||
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
@@ -531,7 +530,6 @@ This project uses three complementary test types. Use the right level for the jo
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
|
||||
- Integration tests
|
||||
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
|
||||
2
.github/workflows/cd-android-live-update.yml
vendored
2
.github/workflows/cd-android-live-update.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: CD Android Live Update
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'android/capawesome.json'
|
||||
- '.github/workflows/cd-android-live-update.yml'
|
||||
|
||||
2
.github/workflows/cd-api.yml
vendored
2
.github/workflows/cd-api.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: API Release
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'backend/api/package.json'
|
||||
- '.github/workflows/cd-api.yml'
|
||||
|
||||
4
.github/workflows/ci-e2e.yml
vendored
4
.github/workflows/ci-e2e.yml
vendored
@@ -2,9 +2,9 @@ name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -2,9 +2,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -522,7 +522,6 @@ How we apply it here
|
||||
This project uses three complementary test types. Use the right level for the job:
|
||||
|
||||
- Unit tests
|
||||
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
@@ -531,7 +530,6 @@ This project uses three complementary test types. Use the right level for the jo
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
|
||||
- Integration tests
|
||||
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"bracketSpacing": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"plugins": ["prettier-plugin-sql"],
|
||||
"plugins": ["prettier-plugin-sql", "prettier-plugin-packagejson"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.sql",
|
||||
|
||||
@@ -1,66 +1,44 @@
|
||||
{
|
||||
"name": "@compass/api",
|
||||
"description": "Backend API endpoints",
|
||||
"version": "1.19.0",
|
||||
"private": true,
|
||||
"description": "Backend API endpoints",
|
||||
"main": "src/serve.ts",
|
||||
"scripts": {
|
||||
"watch:serve": "tsx watch src/serve.ts",
|
||||
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
|
||||
"dev": "yarn watch:serve",
|
||||
"prod": "npx concurrently -n COMPILE,SERVER -c cyan,green \"yarn watch:compile\" \"yarn watch:serve\"",
|
||||
"build": "yarn compile && yarn dist:clean && yarn dist:copy",
|
||||
"build:fast": "yarn compile && yarn dist:copy",
|
||||
"clean": "rm -rf lib && (cd ../../common && rm -rf lib) && (cd ../shared && rm -rf lib) && (cd ../email && rm -rf lib)",
|
||||
"compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias) && cp -r src/public/ lib/",
|
||||
"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\"",
|
||||
"dev": "yarn watch:serve",
|
||||
"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 ../../common/messages/ dist/common/messages/ && 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 package.json dist/backend/api && cp metadata.json dist && cp metadata.json dist/backend/api",
|
||||
"watch": "tsc -w",
|
||||
"lint": "npx eslint . --max-warnings 0",
|
||||
"lint-fix": "npx eslint . --fix",
|
||||
"typecheck": "yarn build && npx tsc --noEmit",
|
||||
"prod": "npx concurrently -n COMPILE,SERVER -c cyan,green \"yarn watch:compile\" \"yarn watch:serve\"",
|
||||
"regen-types": "cd ../supabase && make ENV=prod regen-types",
|
||||
"regen-types-dev": "cd ../supabase && make ENV=dev regen-types-dev",
|
||||
"test": "jest --config jest.config.ts",
|
||||
"test:coverage": "jest --config jest.config.ts --coverage"
|
||||
"test:coverage": "jest --config jest.config.ts --coverage",
|
||||
"typecheck": "yarn build && npx tsc --noEmit",
|
||||
"watch:compile": "npx concurrently \"tsc -b --watch --preserveWatchOutput\" \"(cd ../../common && tsc-alias --watch)\" \"(cd ../shared && tsc-alias --watch)\" \"(cd ../email && tsc-alias --watch)\" \"tsc-alias --watch\"",
|
||||
"watch:serve": "tsx watch src/serve.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"main": "src/serve.ts",
|
||||
"dependencies": {
|
||||
"@google-cloud/monitoring": "4.0.0",
|
||||
"@google-cloud/secret-manager": "4.2.1",
|
||||
"@react-email/components": "0.0.33",
|
||||
"@supabase/supabase-js": "2.38.5",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/extension-blockquote": "2.10.4",
|
||||
"@tiptap/extension-bold": "2.10.4",
|
||||
"@tiptap/extension-bubble-menu": "2.10.4",
|
||||
"@tiptap/extension-floating-menu": "2.10.4",
|
||||
"@tiptap/extension-image": "2.10.4",
|
||||
"@tiptap/extension-link": "2.10.4",
|
||||
"@tiptap/extension-mention": "2.10.4",
|
||||
"@tiptap/html": "2.10.4",
|
||||
"@tiptap/pm": "2.10.4",
|
||||
"@tiptap/starter-kit": "2.10.4",
|
||||
"@tiptap/suggestion": "2.10.4",
|
||||
"colors": "1.4.0",
|
||||
"cors": "2.8.5",
|
||||
"dayjs": "1.11.4",
|
||||
"dayjs": "1.11.19",
|
||||
"express": "5.0.0",
|
||||
"firebase-admin": "13.5.0",
|
||||
"gcp-metadata": "6.1.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"lodash": "4.17.23",
|
||||
"openapi-types": "12.1.3",
|
||||
"pg-promise": "11.5.5",
|
||||
"pg-promise": "12.6.1",
|
||||
"posthog-node": "4.11.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"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",
|
||||
@@ -71,10 +49,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "2.8.17",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.0",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.5.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "email dev --port 3001",
|
||||
"build": "tsc -b",
|
||||
"test": "jest --config jest.config.ts --passWithNoTests",
|
||||
"dev": "email dev --port 3001",
|
||||
"lint": "npx eslint . --max-warnings 0",
|
||||
"lint-fix": "npx eslint . --fix",
|
||||
"test": "jest --config jest.config.ts --passWithNoTests",
|
||||
"typecheck": "npx tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.33",
|
||||
"@react-email/components": "1.0.8",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-icons": "5.5.0",
|
||||
"resend": "4.1.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"resend": "4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/html-to-text": "9.0.4",
|
||||
"@types/prismjs": "1.26.5",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0"
|
||||
"@types/react": "19.2.3",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,20 @@
|
||||
"name": "shared",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsc -b && yarn --cwd=../../common tsc-alias && tsc-alias",
|
||||
"compile": "tsc -b",
|
||||
"lint": "npx eslint . --max-warnings 0",
|
||||
"lint-fix": "npx eslint . --fix",
|
||||
"typecheck": "npx tsc --noEmit",
|
||||
"test": "jest --config jest.config.ts --passWithNoTests"
|
||||
"test": "jest --config jest.config.ts --passWithNoTests",
|
||||
"typecheck": "npx tsc --noEmit"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@google-cloud/monitoring": "4.0.0",
|
||||
"@google-cloud/secret-manager": "4.2.1",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/html": "2.10.4",
|
||||
"colors": "1.4.0",
|
||||
"dayjs": "1.11.4",
|
||||
"colorette": "2.0.20",
|
||||
"dayjs": "1.11.19",
|
||||
"firebase-admin": "13.5.0",
|
||||
"gcp-metadata": "6.1.0",
|
||||
"lodash": "4.17.23",
|
||||
@@ -25,5 +23,11 @@
|
||||
"pg-query-stream": "4.12.0",
|
||||
"posthog-node": "4.11.0",
|
||||
"string-similarity": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"jest": "^29",
|
||||
"ts-jest": "29.4.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {format} from 'node:util'
|
||||
|
||||
import {dim, red, yellow} from 'colors/safe'
|
||||
import {dim, red, yellow} from 'colorette'
|
||||
import {IS_GOOGLE_CLOUD} from 'common/hosting/constants'
|
||||
import {isError, omit, pick} from 'lodash'
|
||||
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsc -b && tsc-alias",
|
||||
"compile": "tsc -b",
|
||||
"lint": "npx eslint . --max-warnings 0",
|
||||
"lint-fix": "npx eslint . --fix",
|
||||
"typecheck": "npx tsc --noEmit",
|
||||
"test": "jest --config jest.config.ts --passWithNoTests"
|
||||
"test": "jest --config jest.config.ts --passWithNoTests",
|
||||
"typecheck": "npx tsc --noEmit"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "2.38.5",
|
||||
"@supabase/supabase-js": "2.98.0",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/extension-image": "2.10.4",
|
||||
"@tiptap/extension-link": "2.10.4",
|
||||
@@ -20,17 +20,16 @@
|
||||
"@tiptap/pm": "2.10.4",
|
||||
"@tiptap/starter-kit": "2.10.4",
|
||||
"@tiptap/suggestion": "2.10.4",
|
||||
"dayjs": "1.11.4",
|
||||
"dayjs": "1.11.19",
|
||||
"lodash": "4.17.23",
|
||||
"string-similarity": "4.0.4",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.2.4",
|
||||
"@types/lodash": "4.14.178",
|
||||
"@types/jest": "^29",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/string-similarity": "4.0.0",
|
||||
"jest": "29.3.1",
|
||||
"supabase": "2.15.8",
|
||||
"jest": "^29",
|
||||
"ts-jest": "29.4.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export type Row<T extends Selectable> = T extends TableName
|
||||
: never
|
||||
export type Column<T extends Selectable> = keyof Row<T> & string
|
||||
|
||||
export type SupabaseClient = SupabaseClientGeneric<Database, 'public', Schema>
|
||||
export type SupabaseClient = SupabaseClientGeneric<Database, 'public'>
|
||||
|
||||
export function createClient(
|
||||
instanceIdOrUrl: string,
|
||||
|
||||
@@ -25,7 +25,6 @@ How we apply it here
|
||||
This project uses three complementary test types. Use the right level for the job:
|
||||
|
||||
- Unit tests
|
||||
|
||||
- Purpose: Verify a single function/module in isolation; fast, deterministic.
|
||||
- Where: Each package under `tests/unit` (e.g., `backend/api/tests/unit`, `web/tests/unit`, `common/tests/unit`,
|
||||
etc.).
|
||||
@@ -34,7 +33,6 @@ This project uses three complementary test types. Use the right level for the jo
|
||||
- When to use: Pure logic, utilities, hooks, reducers, small components with mocked dependencies.
|
||||
|
||||
- Integration tests
|
||||
|
||||
- Purpose: Verify multiple units working together (e.g., function + DB/client, component + context/provider) without
|
||||
spinning up the full app.
|
||||
- Where: Each package under `tests/integration` (e.g., `backend/shared/tests/integration`, `web/tests/integration`).
|
||||
|
||||
@@ -92,7 +92,7 @@ Compass/
|
||||
| Category | Technology | Version |
|
||||
| ---------- | --------------------- | ------- |
|
||||
| Framework | Next.js | 14.1.0 |
|
||||
| UI Library | React | 18.2.0 |
|
||||
| UI Library | React | 19.2.3 |
|
||||
| Language | TypeScript | 5.5.4 |
|
||||
| Styling | Tailwind CSS | 3.3.3 |
|
||||
| State | React Context + Hooks | - |
|
||||
@@ -183,37 +183,40 @@ User Sign-In:
|
||||
|
||||
```sql
|
||||
-- Users table
|
||||
users (
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
email TEXT,
|
||||
created_at TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
)
|
||||
);
|
||||
|
||||
-- Profile information
|
||||
profiles (
|
||||
CREATE TABLE profiles (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users,
|
||||
name TEXT,
|
||||
age INTEGER,
|
||||
bio TEXT,
|
||||
bio TEXT
|
||||
-- many more fields
|
||||
)
|
||||
);
|
||||
|
||||
-- Private user data
|
||||
private_users (
|
||||
CREATE TABLE private_users (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users,
|
||||
email TEXT,
|
||||
notification_settings JSONB
|
||||
)
|
||||
);
|
||||
|
||||
-- Messages
|
||||
private_user_messages (
|
||||
CREATE TABLE private_user_messages (
|
||||
id UUID PRIMARY KEY,
|
||||
from_user_id UUID,
|
||||
to_user_id UUID,
|
||||
content TEXT,
|
||||
created_at TIMESTAMP
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
See `supabase/migrations/` for full schema.
|
||||
@@ -245,18 +248,16 @@ See `supabase/migrations/` for full schema.
|
||||
|
||||
```typescript
|
||||
// Success
|
||||
{
|
||||
// Response data
|
||||
return {
|
||||
// Response data
|
||||
}
|
||||
|
||||
// Error
|
||||
{
|
||||
error: {
|
||||
status: 400,
|
||||
message
|
||||
:
|
||||
"Error description"
|
||||
}
|
||||
return {
|
||||
error: {
|
||||
status: 400,
|
||||
message: 'Error description',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -271,14 +272,21 @@ See `supabase/migrations/` for full schema.
|
||||
|
||||
### State Persistence
|
||||
|
||||
In-memory (lost on refresh)
|
||||
|
||||
```typescript
|
||||
// In-memory (lost on refresh)
|
||||
const [state, setState] = useState(initialValue)
|
||||
```
|
||||
|
||||
// Local storage (persists)
|
||||
Local storage (persists)
|
||||
|
||||
```typescript
|
||||
const [state, setState] = usePersistentLocalState(initialValue, 'key')
|
||||
```
|
||||
|
||||
// Session storage (persists until tab closed)
|
||||
Session storage (persists until tab closed)
|
||||
|
||||
```typescript
|
||||
const [state, setState] = usePersistentInMemoryState(initialValue, 'key')
|
||||
```
|
||||
|
||||
|
||||
166
package.json
166
package.json
@@ -11,95 +11,40 @@
|
||||
"web"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "yarn --cwd=web lint; yarn --cwd=common lint; yarn --cwd=backend/api lint; yarn --cwd=backend/shared lint; yarn --cwd=backend/email lint",
|
||||
"lint-fix": "yarn --cwd=web lint-fix; yarn --cwd=common lint-fix; yarn --cwd=backend/api lint-fix; yarn --cwd=backend/shared lint-fix; yarn --cwd=backend/email lint-fix",
|
||||
"typecheck": "yarn --cwd=web typecheck; yarn --cwd=backend/api typecheck; yarn --cwd=common typecheck; yarn --cwd=backend/shared typecheck; yarn --cwd=backend/email typecheck",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier:check": "prettier --check .",
|
||||
"android-live-update": "./scripts/android_live_update.sh",
|
||||
"build-sync-android": "./scripts/build_sync_android.sh",
|
||||
"build-web-view": "./scripts/build_web_view.sh",
|
||||
"clean-install": "./scripts/install.sh",
|
||||
"dev": "./scripts/run_local.sh dev",
|
||||
"dev:isolated": "./scripts/run_local_isolated.sh",
|
||||
"prod": "./scripts/run_local.sh prod",
|
||||
"clean-install": "./scripts/install.sh",
|
||||
"build-web-view": "./scripts/build_web_view.sh",
|
||||
"build-sync-android": "./scripts/build_sync_android.sh",
|
||||
"android-live-update": "./scripts/android_live_update.sh",
|
||||
"sync-android": "./scripts/sync_android.sh",
|
||||
"emulate": "firebase emulators:start --only auth,storage --project compass-57c3c",
|
||||
"postinstall": "./scripts/post_install.sh",
|
||||
"lint": "yarn --cwd=web lint; yarn --cwd=common lint; yarn --cwd=backend/api lint; yarn --cwd=backend/shared lint; yarn --cwd=backend/email lint",
|
||||
"lint-fix": "yarn --cwd=web lint-fix; yarn --cwd=common lint-fix; yarn --cwd=backend/api lint-fix; yarn --cwd=backend/shared lint-fix; yarn --cwd=backend/email lint-fix",
|
||||
"migrate": "./scripts/migrate.sh",
|
||||
"test": "yarn workspaces run test",
|
||||
"test:coverage": "yarn workspaces run test --coverage",
|
||||
"test:watch": "yarn workspaces run test --watch",
|
||||
"test:update": "yarn workspaces run test --updateSnapshot",
|
||||
"test:e2e": "./scripts/e2e.sh",
|
||||
"test:e2e:dev": "./scripts/e2e-dev.sh",
|
||||
"test:e2e:ui": "./scripts/e2e.sh --ui",
|
||||
"test:e2e:debug": "./scripts/e2e.sh --debug",
|
||||
"test:e2e:services": "./scripts/e2e_services.sh",
|
||||
"test:db:reset": "./scripts/test_db_reset.sh",
|
||||
"test:db:reset-postgres": "docker compose -f scripts/docker-compose.test.yml down -v && docker compose -f scripts/docker-compose.test.yml up -d",
|
||||
"test:db:migrate": "./scripts/test_db_migration.sh",
|
||||
"test:db:seed": "./scripts/seed.sh",
|
||||
"playwright": "playwright test",
|
||||
"playwright:ui": "playwright test --ui",
|
||||
"playwright:debug": "playwright test --debug",
|
||||
"playwright:report": "npx playwright show-report tests/reports/playwright-report",
|
||||
"postinstall": "./scripts/post_install.sh",
|
||||
"emulate": "firebase emulators:start --only auth,storage --project compass-57c3c",
|
||||
"prepare": "npx husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/app": "7.1.0",
|
||||
"@capacitor/core": "7.4.4",
|
||||
"@capacitor/keyboard": "7.0.3",
|
||||
"@capacitor/push-notifications": "7.0.3",
|
||||
"@capacitor/status-bar": "7.0.3",
|
||||
"@capawesome/capacitor-live-update": "7.2.2",
|
||||
"@capgo/capacitor-social-login": "7.14.9",
|
||||
"colorette": "^2.0.20",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"supabase": "2.76.9",
|
||||
"wait-on": "9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "7.4.4",
|
||||
"@capacitor/assets": "3.0.5",
|
||||
"@capacitor/cli": "7.4.4",
|
||||
"@faker-js/faker": "10.1.0",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "29.2.4",
|
||||
"@types/node": "20.12.11",
|
||||
"@eslint/js": "^9.39.0",
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"@next/eslint-plugin-next": "^15.3.3",
|
||||
"chalk": "5.6.2",
|
||||
"concurrently": "8.2.2",
|
||||
"dotenv-cli": "10.0.0",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-plugin-lodash": "7.4.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"firebase-tools": "14.27.0",
|
||||
"husky": "9.1.7",
|
||||
"jest": "29.3.1",
|
||||
"nodemon": "2.0.20",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-sql": "0.19.2",
|
||||
"prettier-plugin-tailwindcss": "0.2.8",
|
||||
"ts-jest": "29.4.6",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.5.4"
|
||||
"playwright:ui": "playwright test --ui",
|
||||
"prepare": "npx husky",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier:check": "prettier --check .",
|
||||
"prod": "./scripts/run_local.sh prod",
|
||||
"sync-android": "./scripts/sync_android.sh",
|
||||
"test": "yarn workspaces run test",
|
||||
"test:coverage": "yarn workspaces run test --coverage",
|
||||
"test:db:migrate": "./scripts/test_db_migration.sh",
|
||||
"test:db:reset": "./scripts/test_db_reset.sh",
|
||||
"test:db:reset-postgres": "docker compose -f scripts/docker-compose.test.yml down -v && docker compose -f scripts/docker-compose.test.yml up -d",
|
||||
"test:db:seed": "./scripts/seed.sh",
|
||||
"test:e2e": "./scripts/e2e.sh",
|
||||
"test:e2e:debug": "./scripts/e2e.sh --debug",
|
||||
"test:e2e:dev": "./scripts/e2e-dev.sh",
|
||||
"test:e2e:services": "./scripts/e2e_services.sh",
|
||||
"test:e2e:ui": "./scripts/e2e.sh --ui",
|
||||
"test:update": "yarn workspaces run test --updateSnapshot",
|
||||
"test:watch": "yarn workspaces run test --watch",
|
||||
"typecheck": "yarn --cwd=web typecheck; yarn --cwd=backend/api typecheck; yarn --cwd=common typecheck; yarn --cwd=backend/shared typecheck; yarn --cwd=backend/email typecheck"
|
||||
},
|
||||
"resolutions": {
|
||||
"@tiptap/core": "2.10.4",
|
||||
@@ -107,19 +52,62 @@
|
||||
"@tiptap/extension-bold": "2.10.4",
|
||||
"@tiptap/extension-bubble-menu": "2.10.4",
|
||||
"@tiptap/extension-floating-menu": "2.10.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.10.4",
|
||||
"@tiptap/extension-image": "2.10.4",
|
||||
"@tiptap/extension-link": "2.10.4",
|
||||
"@tiptap/extension-mention": "2.10.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.10.4",
|
||||
"@tiptap/html": "2.10.4",
|
||||
"@tiptap/pm": "2.10.4",
|
||||
"@tiptap/starter-kit": "2.10.4",
|
||||
"@tiptap/suggestion": "2.10.4",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/jest": "^29",
|
||||
"@types/node": "20.19.35",
|
||||
"@types/react": "19.2.3",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"jest": "^29",
|
||||
"lodash": "4.17.23",
|
||||
"pg-query-stream/pg": "8.x",
|
||||
"prosemirror-model": "1.x",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"pg-query-stream/pg": "8.x"
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@faker-js/faker": "10.1.0",
|
||||
"@next/eslint-plugin-next": "16.1.6",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.4",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^29",
|
||||
"@types/node": "20.19.35",
|
||||
"concurrently": "8.2.2",
|
||||
"dotenv-cli": "10.0.0",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-lodash": "7.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"firebase-tools": "14.27.0",
|
||||
"husky": "9.1.7",
|
||||
"jest": "^29",
|
||||
"nodemon": "3.1.14",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-packagejson": "3.0.0",
|
||||
"prettier-plugin-sql": "0.19.2",
|
||||
"prettier-plugin-tailwindcss": "0.2.8",
|
||||
"supabase": "2.76.9",
|
||||
"ts-jest": "29.4.6",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.5.4",
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"wait-on": "9.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ This is the frontend of the Compass platform, a transparent platform for forming
|
||||
|
||||
- **Framework**: Next.js 14.1.0
|
||||
- **Language**: TypeScript
|
||||
- **UI Library**: React 18.2.0
|
||||
- **UI Library**: React 19.2.3
|
||||
- **Styling**: Tailwind CSS 3.3.3
|
||||
- **State Management**: React Context + Custom Hooks
|
||||
- **Forms**: React Hook Form
|
||||
@@ -220,7 +220,6 @@ Catches React errors and shows user-friendly message:
|
||||
|
||||
```tsx
|
||||
import {ErrorBoundary} from 'web/components/error-boundary'
|
||||
|
||||
;<ErrorBoundary>
|
||||
<MyComponent />
|
||||
</ErrorBoundary>
|
||||
@@ -250,7 +249,6 @@ Keyboard users can skip to main content:
|
||||
|
||||
```tsx
|
||||
import {SkipLink, MainContent} from 'web/components/skip-link'
|
||||
|
||||
;<>
|
||||
<SkipLink />
|
||||
<MainContent>...</MainContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PlusIcon, XIcon} from '@heroicons/react/outline'
|
||||
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import {MAX_ANSWER_LENGTH} from 'common/envs/constants'
|
||||
import {debug} from 'common/logger'
|
||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
|
||||
@@ -156,7 +156,9 @@ function CreateCompatibilityModalContent(props: {
|
||||
<ExpandingInput
|
||||
maxLength={MAX_COMPATIBILITY_QUESTION_LENGTH}
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value || '')}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setQuestion(e.target.value || '')
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="gap-1">
|
||||
@@ -169,7 +171,9 @@ function CreateCompatibilityModalContent(props: {
|
||||
<div key={index} className="relative">
|
||||
<ExpandingInput
|
||||
value={options[index]}
|
||||
onChange={(e) => onOptionChange(index, e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onOptionChange(index, e.target.value)
|
||||
}
|
||||
className="w-full"
|
||||
placeholder={t('answers.add.option_placeholder', 'Option {n}', {
|
||||
n: String(index + 1),
|
||||
@@ -182,7 +186,7 @@ function CreateCompatibilityModalContent(props: {
|
||||
className="bg-ink-400 text-ink-0 hover:bg-ink-600 transition-color absolute -right-1.5 -top-1.5 rounded-full p-0.5"
|
||||
onClick={() => deleteOption(index)}
|
||||
>
|
||||
<XIcon className="z-10 h-3 w-3" />
|
||||
<XMarkIcon className="z-10 h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {RadioGroup} from '@headlessui/react'
|
||||
import {UserIcon} from '@heroicons/react/solid'
|
||||
import {UserIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
@@ -221,7 +221,9 @@ export function AnswerCompatibilityQuestionContent(props: {
|
||||
className={'w-full'}
|
||||
rows={3}
|
||||
value={answer.explanation ?? ''}
|
||||
onChange={(e) => setAnswer({...answer, explanation: e.target.value})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setAnswer({...answer, explanation: e.target.value})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CheckCircleIcon, XCircleIcon} from '@heroicons/react/outline'
|
||||
import {CheckCircleIcon, XCircleIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PencilIcon, TrashIcon} from '@heroicons/react/outline'
|
||||
import {PencilIcon, TrashIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
getAnswerCompatibility,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ArrowLeftIcon, PlusIcon} from '@heroicons/react/outline'
|
||||
import {ArrowLeftIcon, PlusIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {User} from 'common/user'
|
||||
import {TbMessage} from 'react-icons/tb'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PencilIcon, XIcon} from '@heroicons/react/outline'
|
||||
import {PencilIcon, XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
@@ -122,7 +122,7 @@ function AnswerBlock(props: {
|
||||
},
|
||||
{
|
||||
name: t('answers.menu.delete', 'Delete'),
|
||||
icon: <XIcon className="h-5 w-5" />,
|
||||
icon: <XMarkIcon className="h-5 w-5" />,
|
||||
onClick: () => deleteAnswer(answer, user.id).then(() => refreshAnswers()),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PencilIcon} from '@heroicons/react/outline'
|
||||
import {PencilIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {capitalize, orderBy} from 'lodash'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ArrowLeftIcon} from '@heroicons/react/solid'
|
||||
import {ArrowLeftIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import {useEffect, useState} from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PencilIcon, XIcon} from '@heroicons/react/outline'
|
||||
import {PencilIcon, XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import {JSONContent} from '@tiptap/core'
|
||||
import clsx from 'clsx'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
@@ -61,7 +61,7 @@ export function BioBlock(props: {
|
||||
},
|
||||
{
|
||||
name: t('profile.bio.delete', 'Delete'),
|
||||
icon: <XIcon className="h-5 w-5" />,
|
||||
icon: <XMarkIcon className="h-5 w-5" />,
|
||||
onClick: async () => {
|
||||
const {error} = await tryCatch(updateProfile({bio: null}))
|
||||
if (error) console.error(error)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {QuestionMarkCircleIcon} from '@heroicons/react/outline'
|
||||
import {QuestionMarkCircleIcon} from '@heroicons/react/24/outline'
|
||||
import {JSONContent} from '@tiptap/core'
|
||||
import {MAX_INT, MIN_BIO_LENGTH} from 'common/constants'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
// className={'!h-10 max-w-[200px] self-end text-sm'}
|
||||
// value={query}
|
||||
// placeholder={'Search name'}
|
||||
// onChange={(e) => {
|
||||
// onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
// setQuery(e.target.value)
|
||||
// }}
|
||||
// />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx'
|
||||
import {forwardRef, MouseEventHandler, ReactNode, Ref} from 'react'
|
||||
import {ComponentPropsWithoutRef, forwardRef, MouseEventHandler, ReactNode, Ref} from 'react'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
|
||||
export type SizeType = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
@@ -75,7 +75,7 @@ export const Button = forwardRef(function Button(
|
||||
color?: ColorType | null
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
loading?: boolean
|
||||
} & JSX.IntrinsicElements['button'],
|
||||
} & ComponentPropsWithoutRef<'button'>,
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) {
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx'
|
||||
import {ReactNode, useState} from 'react'
|
||||
import {ReactElement, ReactNode, useState} from 'react'
|
||||
|
||||
import {Col} from '../layout/col'
|
||||
import {Modal} from '../layout/modal'
|
||||
@@ -9,7 +9,7 @@ import {Button, ColorType, SizeType} from './button'
|
||||
export function ConfirmationButton(props: {
|
||||
openModalBtn: {
|
||||
label: string
|
||||
icon?: JSX.Element
|
||||
icon?: ReactElement
|
||||
className?: string
|
||||
color?: ColorType
|
||||
size?: SizeType
|
||||
@@ -84,7 +84,7 @@ export function ConfirmationButton(props: {
|
||||
|
||||
<Button
|
||||
className={openModalBtn.className}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (disabled) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {CheckIcon, ClipboardCopyIcon, DuplicateIcon} from '@heroicons/react/outline'
|
||||
import {LinkIcon} from '@heroicons/react/solid'
|
||||
import {CheckIcon, ClipboardIcon, DocumentDuplicateIcon} from '@heroicons/react/24/outline'
|
||||
import {LinkIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {ComponentProps, useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
@@ -121,7 +121,11 @@ export const CopyLinkRow = (props: {
|
||||
<div className={'select-all truncate'}>{displayUrl}</div>
|
||||
{url && (
|
||||
<div className={linkButtonClassName}>
|
||||
{!iconPressed ? <DuplicateIcon className="h-5 w-5" /> : <CheckIcon className="h-5 w-5" />}
|
||||
{!iconPressed ? (
|
||||
<DocumentDuplicateIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
@@ -152,7 +156,7 @@ export function SimpleCopyTextButton(props: {
|
||||
noTap
|
||||
placement="bottom"
|
||||
>
|
||||
<ClipboardCopyIcon className={'h-5'} aria-hidden="true" />
|
||||
<ClipboardIcon className={'h-5'} aria-hidden="true" />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ export function FileUploadButton(props: {
|
||||
accept=".gif,.jpg,.jpeg,.png,.webp, image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (files) {
|
||||
onFiles(Array.from(files))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DotsHorizontalIcon} from '@heroicons/react/outline'
|
||||
import {EllipsisHorizontalIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {User} from 'common/user'
|
||||
import {buildArray} from 'common/util/array'
|
||||
@@ -47,7 +47,7 @@ export function MoreOptionsUserButton(props: {user: User}) {
|
||||
className="rounded-none px-6"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
<DotsHorizontalIcon className={clsx('h-5 w-5 flex-shrink-0')} aria-hidden="true" />
|
||||
<EllipsisHorizontalIcon className={clsx('h-5 w-5 flex-shrink-0')} aria-hidden="true" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {DotsHorizontalIcon, PencilIcon, TrashIcon} from '@heroicons/react/outline'
|
||||
import {EmojiHappyIcon} from '@heroicons/react/solid'
|
||||
import {EllipsisHorizontalIcon, PencilIcon, TrashIcon} from '@heroicons/react/24/outline'
|
||||
import {FaceSmileIcon} from '@heroicons/react/24/solid'
|
||||
import {JSONContent} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {PrivateChatMessage} from 'common/chat-message'
|
||||
@@ -111,7 +111,7 @@ export function MessageActions(props: {
|
||||
},
|
||||
{
|
||||
name: t('messages.action.add_reaction', 'Add Reaction'),
|
||||
icon: <EmojiHappyIcon className="h-4 w-4" />,
|
||||
icon: <FaceSmileIcon className="h-4 w-4" />,
|
||||
onClick: () => {
|
||||
setShowEmojiPicker(!showEmojiPicker)
|
||||
},
|
||||
@@ -119,7 +119,7 @@ export function MessageActions(props: {
|
||||
].filter(Boolean) as DropdownItem[]
|
||||
}
|
||||
closeOnClick={true}
|
||||
icon={<DotsHorizontalIcon className="h-5 w-5 text-gray-500" />}
|
||||
icon={<EllipsisHorizontalIcon className="h-5 w-5 text-gray-500" />}
|
||||
menuWidth="w-40"
|
||||
className="text-ink-500 hover:text-ink-700 rounded-full p-1 hover:bg-canvas-50"
|
||||
/>
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import {Comment} from 'common/comment'
|
||||
import {run} from 'common/supabase/utils'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Modal} from 'web/components/layout/modal'
|
||||
import {Content} from 'web/components/widgets/editor'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {useIsClient} from 'web/hooks/use-is-client'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
import {shortenedFromNow} from 'web/lib/util/shortenedFromNow'
|
||||
import {formatTimeShort} from 'web/lib/util/time'
|
||||
|
||||
import {DateTimeTooltip} from '../widgets/datetime-tooltip'
|
||||
|
||||
type EditHistory = Comment & {
|
||||
editCreatedTime: number
|
||||
}
|
||||
export const CommentEditHistoryButton = (props: {comment: Comment}) => {
|
||||
const {comment} = props
|
||||
const [showEditHistory, setShowEditHistory] = useState(false)
|
||||
const [edits, setEdits] = useState<EditHistory[] | undefined>(undefined)
|
||||
|
||||
const isClient = useIsClient()
|
||||
const t = useT()
|
||||
|
||||
const loadEdits = async () => {
|
||||
const {data} = await run(
|
||||
db
|
||||
.from('contract_comment_edits')
|
||||
.select('*')
|
||||
.eq('comment_id', comment.id)
|
||||
.order('created_time', {ascending: false}),
|
||||
)
|
||||
|
||||
// created_time is the time the row is created, but the row's content is the content before the edit, aka created_time is when the content is deleted and replaced
|
||||
const comments = data.map((edit, i) => {
|
||||
const comment = edit.data as Comment
|
||||
const editCreatedTime =
|
||||
i === data.length - 1 ? comment.createdTime : new Date(data[i + 1].created_time).valueOf()
|
||||
|
||||
return {...comment, editCreatedTime}
|
||||
})
|
||||
|
||||
setEdits(comments)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (showEditHistory && edits === undefined) {
|
||||
loadEdits()
|
||||
}
|
||||
}, [showEditHistory])
|
||||
|
||||
if (!comment.editedTime) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<DateTimeTooltip time={comment.editedTime} placement={'top'}>
|
||||
<button
|
||||
className={
|
||||
'text-ink-400 hover:bg-ink-50 mx-1 inline-block whitespace-nowrap rounded px-0.5 text-sm'
|
||||
}
|
||||
onClick={() => setShowEditHistory(true)}
|
||||
>
|
||||
(edited) {isClient && shortenedFromNow(comment.editedTime, t)}
|
||||
</button>
|
||||
</DateTimeTooltip>
|
||||
<Modal size={'md'} open={showEditHistory} setOpen={setShowEditHistory}>
|
||||
<div className={'bg-canvas-50 rounded-2xl p-4'}>
|
||||
<Title className="w-full text-center">Edit History</Title>
|
||||
{!edits ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<Col className="gap-4">
|
||||
{edits.map((edit) => (
|
||||
<Col key={edit.id} className={'bg-ink-100 gap-2 rounded-xl rounded-tl-none p-2'}>
|
||||
<div className="text-ink-500 text-sm">
|
||||
{formatTimeShort(edit.editCreatedTime)}
|
||||
</div>
|
||||
<Content size="sm" className="mt-1 grow" content={edit.content || edit.text} />
|
||||
</Col>
|
||||
))}
|
||||
</Col>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
// import {Comment} from 'common/comment'
|
||||
// import {run} from 'common/supabase/utils'
|
||||
// import {useEffect, useState} from 'react'
|
||||
// import {Col} from 'web/components/layout/col'
|
||||
// import {Modal} from 'web/components/layout/modal'
|
||||
// import {Content} from 'web/components/widgets/editor'
|
||||
// import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
// import {Title} from 'web/components/widgets/title'
|
||||
// import {useIsClient} from 'web/hooks/use-is-client'
|
||||
// import {useT} from 'web/lib/locale'
|
||||
// import {db} from 'web/lib/supabase/db'
|
||||
// import {shortenedFromNow} from 'web/lib/util/shortenedFromNow'
|
||||
// import {formatTimeShort} from 'web/lib/util/time'
|
||||
//
|
||||
// import {DateTimeTooltip} from '../widgets/datetime-tooltip'
|
||||
//
|
||||
// type EditHistory = Comment & {
|
||||
// editCreatedTime: number
|
||||
// }
|
||||
// export const CommentEditHistoryButton = (props: {comment: Comment}) => {
|
||||
// const {comment} = props
|
||||
// const [showEditHistory, setShowEditHistory] = useState(false)
|
||||
// const [edits, setEdits] = useState<EditHistory[] | undefined>(undefined)
|
||||
//
|
||||
// const isClient = useIsClient()
|
||||
// const t = useT()
|
||||
//
|
||||
// const loadEdits = async () => {
|
||||
// const {data} = await run(
|
||||
// db
|
||||
// .from('contract_comment_edits')
|
||||
// .select('*')
|
||||
// .eq('comment_id', comment.id)
|
||||
// .order('created_time', {ascending: false}),
|
||||
// )
|
||||
//
|
||||
// // created_time is the time the row is created, but the row's content is the content before the edit, aka created_time is when the content is deleted and replaced
|
||||
// const comments = data.map((edit, i) => {
|
||||
// const comment = edit.data as Comment
|
||||
// const editCreatedTime =
|
||||
// i === data.length - 1 ? comment.createdTime : new Date(data[i + 1].created_time).valueOf()
|
||||
//
|
||||
// return {...comment, editCreatedTime}
|
||||
// })
|
||||
//
|
||||
// setEdits(comments)
|
||||
// }
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (showEditHistory && edits === undefined) {
|
||||
// loadEdits()
|
||||
// }
|
||||
// }, [showEditHistory])
|
||||
//
|
||||
// if (!comment.editedTime) return null
|
||||
//
|
||||
// return (
|
||||
// <>
|
||||
// <DateTimeTooltip time={comment.editedTime} placement={'top'}>
|
||||
// <button
|
||||
// className={
|
||||
// 'text-ink-400 hover:bg-ink-50 mx-1 inline-block whitespace-nowrap rounded px-0.5 text-sm'
|
||||
// }
|
||||
// onClick={() => setShowEditHistory(true)}
|
||||
// >
|
||||
// (edited) {isClient && shortenedFromNow(comment.editedTime, t)}
|
||||
// </button>
|
||||
// </DateTimeTooltip>
|
||||
// <Modal size={'md'} open={showEditHistory} setOpen={setShowEditHistory}>
|
||||
// <div className={'bg-canvas-50 rounded-2xl p-4'}>
|
||||
// <Title className="w-full text-center">Edit History</Title>
|
||||
// {!edits ? (
|
||||
// <LoadingIndicator />
|
||||
// ) : (
|
||||
// <Col className="gap-4">
|
||||
// {edits.map((edit) => (
|
||||
// <Col key={edit.id} className={'bg-ink-100 gap-2 rounded-xl rounded-tl-none p-2'}>
|
||||
// <div className="text-ink-500 text-sm">
|
||||
// {formatTimeShort(edit.editCreatedTime)}
|
||||
// </div>
|
||||
// <Content size="sm" className="mt-1 grow" content={edit.content || edit.text} />
|
||||
// </Col>
|
||||
// ))}
|
||||
// </Col>
|
||||
// )}
|
||||
// </div>
|
||||
// </Modal>
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PaperAirplaneIcon} from '@heroicons/react/solid'
|
||||
import {PaperAirplaneIcon} from '@heroicons/react/24/solid'
|
||||
import {Editor} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {MAX_COMMENT_LENGTH, ReplyToUserInfo} from 'common/comment'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Popover} from '@headlessui/react'
|
||||
import {DotsHorizontalIcon} from '@heroicons/react/solid'
|
||||
import {flip, offset, shift, useFloating} from '@floating-ui/react'
|
||||
import {Popover, PopoverButton, PopoverPanel} from '@headlessui/react'
|
||||
import {EllipsisHorizontalIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {ReactNode, useState} from 'react'
|
||||
import {usePopper} from 'react-popper'
|
||||
import {ReactNode} from 'react'
|
||||
import {AnimationOrNothing} from 'web/components/comments/dropdown-menu'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
|
||||
@@ -27,34 +27,32 @@ export default function DropdownMenu(props: {
|
||||
withinOverflowContainer,
|
||||
buttonContent,
|
||||
} = props
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>()
|
||||
const {styles, attributes} = usePopper(referenceElement, popperElement, {
|
||||
|
||||
const {refs, floatingStyles} = useFloating({
|
||||
strategy: withinOverflowContainer ? 'fixed' : 'absolute',
|
||||
middleware: [offset(8), flip(), shift({padding: 8})],
|
||||
})
|
||||
const icon = props.icon ?? <DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
const icon = props.icon ?? <EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
return (
|
||||
<Popover className={clsx('relative inline-block text-left', className)}>
|
||||
{({open}) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
ref={setReferenceElement}
|
||||
<PopoverButton
|
||||
ref={refs.setReference}
|
||||
className={clsx('text-ink-500 hover:text-ink-800 flex items-center', buttonClass)}
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
<span className="sr-only">Open options</span>
|
||||
{buttonContent ? buttonContent(open) : icon}
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
|
||||
<AnimationOrNothing show={open} animate={!withinOverflowContainer}>
|
||||
<Popover.Panel
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
<PopoverPanel
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
className={clsx(
|
||||
'bg-canvas-0 ring-ink-1000 z-30 mt-2 rounded-md shadow-lg ring-1 ring-opacity-5 focus:outline-none',
|
||||
menuWidth ?? 'w-34',
|
||||
@@ -62,8 +60,8 @@ export default function DropdownMenu(props: {
|
||||
'p-1',
|
||||
)}
|
||||
>
|
||||
<Col className={'gap-1'}>{items.map((item) => item)}</Col>
|
||||
</Popover.Panel>
|
||||
<Col className="gap-1">{items}</Col>
|
||||
</PopoverPanel>
|
||||
</AnimationOrNothing>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {Popover, Transition} from '@headlessui/react'
|
||||
import {ChevronDownIcon, ChevronUpIcon, DotsHorizontalIcon} from '@heroicons/react/solid'
|
||||
import {flip, offset, shift, useFloating} from '@floating-ui/react'
|
||||
import {Popover, PopoverButton, PopoverPanel, Transition} from '@headlessui/react'
|
||||
import {ChevronDownIcon, ChevronUpIcon, EllipsisHorizontalIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {toKey} from 'common/parsing'
|
||||
import {Fragment, ReactNode, useState} from 'react'
|
||||
import {usePopper} from 'react-popper'
|
||||
import {Fragment, ReactNode} from 'react'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {useT} from 'web/lib/locale'
|
||||
@@ -39,33 +39,32 @@ export default function DropdownMenu(props: {
|
||||
withinOverflowContainer,
|
||||
buttonContent,
|
||||
} = props
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>()
|
||||
const {styles, attributes} = usePopper(referenceElement, popperElement, {
|
||||
|
||||
const {refs, floatingStyles} = useFloating({
|
||||
strategy: withinOverflowContainer ? 'fixed' : 'absolute',
|
||||
middleware: [offset(8), flip(), shift({padding: 8})],
|
||||
})
|
||||
const icon = props.icon ?? <DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
const icon = props.icon ?? <EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
|
||||
return (
|
||||
<Popover className={clsx('relative inline-block text-left', className)}>
|
||||
{({open, close}) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
ref={setReferenceElement}
|
||||
<PopoverButton
|
||||
ref={refs.setReference}
|
||||
className={clsx('text-ink-500 hover-bold flex items-center', buttonClass)}
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
<span className="sr-only">Open options</span>
|
||||
{buttonContent ? buttonContent(open) : icon}
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
|
||||
<AnimationOrNothing show={open} animate={!withinOverflowContainer}>
|
||||
<Popover.Panel
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
<PopoverPanel
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
className={clsx(
|
||||
'bg-canvas-0 ring-ink-1000 z-30 mt-2 rounded-md shadow-lg ring-1 ring-opacity-5 focus:outline-none',
|
||||
menuWidth ?? 'w-34',
|
||||
@@ -74,36 +73,33 @@ export default function DropdownMenu(props: {
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.name}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
item.onClick()
|
||||
if (closeOnClick) {
|
||||
close()
|
||||
}
|
||||
}}
|
||||
className={clsx(
|
||||
selectedItemName && item.name == selectedItemName
|
||||
? 'bg-primary-100'
|
||||
: 'hover:bg-canvas-100 hover:text-ink-900',
|
||||
'text-ink-700',
|
||||
'flex w-full gap-2 px-4 py-2 text-left text-sm rounded-md',
|
||||
)}
|
||||
>
|
||||
{item.icon && <div className="w-5">{item.icon}</div>}
|
||||
{item.name}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
key={item.name}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
item.onClick()
|
||||
if (closeOnClick) close()
|
||||
}}
|
||||
className={clsx(
|
||||
selectedItemName && item.name === selectedItemName
|
||||
? 'bg-primary-100'
|
||||
: 'hover:bg-canvas-100 hover:text-ink-900',
|
||||
'text-ink-700 flex w-full gap-2 px-4 py-2 text-left text-sm rounded-md',
|
||||
)}
|
||||
>
|
||||
{item.icon && <div className="w-5">{item.icon}</div>}
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
</AnimationOrNothing>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export const AnimationOrNothing = (props: {
|
||||
animate: boolean
|
||||
show: boolean
|
||||
@@ -134,7 +130,6 @@ export function DropdownOptions(props: {
|
||||
translationPrefix?: string
|
||||
}) {
|
||||
const {items, onClick, activeKey, translationPrefix} = props
|
||||
|
||||
const t = useT()
|
||||
|
||||
const translateOption = (key: string, value: string) => {
|
||||
@@ -143,25 +138,23 @@ export function DropdownOptions(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<Col className={''}>
|
||||
<Col>
|
||||
{Object.entries(items).map(([key, item]) => (
|
||||
<div key={key}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onClick(key)
|
||||
}}
|
||||
className={clsx(
|
||||
key == activeKey ? 'bg-primary-100' : 'hover:bg-canvas-100 hover:text-ink-900',
|
||||
'text-ink-700',
|
||||
'flex w-full gap-2 px-4 py-2 text-left text-sm rounded-md',
|
||||
)}
|
||||
>
|
||||
{item.icon && <div className="w-5">{item.icon}</div>}
|
||||
{translateOption(key, item.label ?? item)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
key={key}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onClick(key)
|
||||
}}
|
||||
className={clsx(
|
||||
key === activeKey ? 'bg-primary-100' : 'hover:bg-canvas-100 hover:text-ink-900',
|
||||
'text-ink-700 flex w-full gap-2 px-4 py-2 text-left text-sm rounded-md',
|
||||
)}
|
||||
>
|
||||
{item.icon && <div className="w-5">{item.icon}</div>}
|
||||
{translateOption(key, item.label ?? item)}
|
||||
</button>
|
||||
))}
|
||||
</Col>
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ export function EmbedModal(props: {
|
||||
className="bg-canvas-50 border-ink-300 placeholder:text-ink-300 focus:border-primary-500 focus:ring-primary-500 block w-full rounded-md shadow-sm sm:text-sm"
|
||||
placeholder="e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
|
||||
/>
|
||||
|
||||
{embed && <div dangerouslySetInnerHTML={{__html: embed}}></div>}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {CheckIcon, LinkIcon, TrashIcon} from '@heroicons/react/solid'
|
||||
import {CheckIcon, LinkIcon, TrashIcon} from '@heroicons/react/24/solid'
|
||||
import {Editor} from '@tiptap/core'
|
||||
import {BubbleMenu} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
@@ -71,7 +71,7 @@ export function FloatingFormatMenu(props: {
|
||||
inputMode="url"
|
||||
className="h-5 border-0 bg-inherit text-sm !shadow-none !ring-0"
|
||||
placeholder="Type or paste a link"
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => (setLink(), setUrl(null))}>
|
||||
<CheckIcon className="h-5 w-5" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {EmojiHappyIcon} from '@heroicons/react/outline'
|
||||
import {CodeIcon, PhotographIcon} from '@heroicons/react/solid'
|
||||
import {FaceSmileIcon} from '@heroicons/react/24/outline'
|
||||
import {CodeBracketIcon, PhotoIcon} from '@heroicons/react/24/solid'
|
||||
import {Editor} from '@tiptap/react'
|
||||
import {MouseEventHandler, useState} from 'react'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
@@ -30,13 +30,13 @@ export function StickyFormatMenu(props: {
|
||||
<ToolbarButton
|
||||
key={'embed-button'}
|
||||
label={t('sticky_format_menu.add_embed', 'Add embed')}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
setIframeOpen(true)
|
||||
}}
|
||||
>
|
||||
<CodeIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<CodeBracketIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</ToolbarButton>
|
||||
)}
|
||||
<ToolbarButton
|
||||
@@ -44,7 +44,7 @@ export function StickyFormatMenu(props: {
|
||||
label={t('sticky_format_menu.add_emoji', 'Add emoji')}
|
||||
onClick={() => insertEmoji(editor)}
|
||||
>
|
||||
<EmojiHappyIcon className="h-5 w-5" />
|
||||
<FaceSmileIcon className="h-5 w-5" />
|
||||
</ToolbarButton>
|
||||
|
||||
<EmbedModal editor={editor} open={iframeOpen} setOpen={setIframeOpen} />
|
||||
@@ -69,7 +69,7 @@ function UploadButton(props: {upload: UploadMutation}) {
|
||||
className="hover:text-ink-700 disabled:text-ink-300 active:text-ink-800 text-ink-400 relative flex rounded px-3 py-1 pl-4 transition-colors"
|
||||
>
|
||||
<Row className={'items-center justify-start gap-2'}>
|
||||
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<PhotoIcon className="h-5 w-5" aria-hidden="true" />
|
||||
{upload?.isLoading && (
|
||||
<LoadingIndicator
|
||||
className="absolute bottom-0 left-0 right-0 top-0"
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {computePosition, flip, offset, shift} from '@floating-ui/dom'
|
||||
import type {MentionOptions} from '@tiptap/extension-mention'
|
||||
import {ReactRenderer} from '@tiptap/react'
|
||||
import {beginsWith} from 'common/util/parse'
|
||||
import {sortBy} from 'lodash'
|
||||
import tippy from 'tippy.js'
|
||||
import {searchUsers} from 'web/lib/supabase/users'
|
||||
|
||||
import {MentionList} from './mention-list'
|
||||
type Render = Suggestion['render']
|
||||
|
||||
type Render = Suggestion['render']
|
||||
type Suggestion = MentionOptions['suggestion']
|
||||
|
||||
// copied from https://tiptap.dev/api/nodes/mention#usage
|
||||
export const mentionSuggestion: Suggestion = {
|
||||
allowedPrefixes: [' '],
|
||||
items: async ({query}) =>
|
||||
@@ -23,53 +22,63 @@ export const mentionSuggestion: Suggestion = {
|
||||
export function makeMentionRender(mentionList: any): Render {
|
||||
return () => {
|
||||
let component: ReactRenderer
|
||||
let popup: ReturnType<typeof tippy>
|
||||
let container: HTMLDivElement
|
||||
|
||||
const updatePosition = (clientRect: (() => DOMRect | null) | null | undefined) => {
|
||||
if (!clientRect || !container) return
|
||||
const rect = clientRect()
|
||||
if (!rect) return
|
||||
|
||||
// Virtual element from the cursor position
|
||||
const virtualEl = {
|
||||
getBoundingClientRect: () => rect,
|
||||
}
|
||||
|
||||
computePosition(virtualEl as Element, container, {
|
||||
placement: 'bottom-start',
|
||||
middleware: [offset(8), flip(), shift({padding: 8})],
|
||||
}).then(({x, y}) => {
|
||||
Object.assign(container.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
onStart(props) {
|
||||
component = new ReactRenderer(mentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: component?.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
})
|
||||
container = document.createElement('div')
|
||||
container.style.cssText = 'position:fixed;z-index:9999;'
|
||||
container.appendChild(component.element)
|
||||
document.body.appendChild(container)
|
||||
|
||||
updatePosition(props.clientRect)
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component?.updateProps(props)
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
popup?.[0].setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
updatePosition(props.clientRect)
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key)
|
||||
if (
|
||||
props.event.key === 'Escape' ||
|
||||
// Also break out of the mention if the tooltip isn't visible
|
||||
(props.event.key === 'Enter' && !popup?.[0].state.isShown)
|
||||
) {
|
||||
popup?.[0].destroy()
|
||||
component?.destroy()
|
||||
return false
|
||||
}
|
||||
if (
|
||||
props.event.key === 'Escape' ||
|
||||
(props.event.key === 'Enter' && !container?.isConnected)
|
||||
) {
|
||||
container?.remove()
|
||||
component?.destroy()
|
||||
return false
|
||||
}
|
||||
return (component?.ref as any)?.onKeyDown(props)
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.[0].destroy()
|
||||
container?.remove()
|
||||
component?.destroy()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export function CreateEventModal(props: {
|
||||
}
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const {name, value} = e.target
|
||||
setFormData((prev) => ({...prev, [name]: value}))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/outline'
|
||||
import {XIcon} from '@heroicons/react/solid'
|
||||
import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/24/outline'
|
||||
import {XMarkIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {formatFilters, SKIPPED_FORMAT_FILTERS_KEYS} from 'common/filters-format'
|
||||
@@ -177,7 +177,7 @@ function SelectedFiltersSummary(props: {
|
||||
onClick={filter.onClear}
|
||||
className="hover:text-gray-500 dark:hover:text-gray-50"
|
||||
>
|
||||
<XIcon className="h-3 w-3" />
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {XIcon} from '@heroicons/react/solid'
|
||||
import {XMarkIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {OriginLocation} from 'common/filters'
|
||||
import {formatDistance, kmToMiles, milesToKm} from 'common/measurement-utils'
|
||||
@@ -115,7 +115,7 @@ export function LocationFilter(props: {
|
||||
<Row className="items-center gap-1">
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
||||
placeholder={t('filter.location.search_city', 'Search city...')}
|
||||
className="h-8 w-full rounded-none border-0 bg-transparent px-1 focus:border-b focus:ring-0 focus:ring-transparent"
|
||||
autoFocus
|
||||
@@ -201,7 +201,7 @@ function LocationResults(props: {
|
||||
className="hover:bg-primary-200 hover:text-ink-950 cursor-pointer px-4 py-2 transition-colors"
|
||||
>
|
||||
<Row className="items-center gap-2">
|
||||
<XIcon className="h-4 w-4 text-ink-400" aria-label={t('common.close', 'Close')} />
|
||||
<XMarkIcon className="h-4 w-4 text-ink-400" aria-label={t('common.close', 'Close')} />
|
||||
<span>{t('filter.location.set_any_city', 'Set to Any City')}</span>
|
||||
</Row>
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function MyMatchesToggle(props: {
|
||||
type="checkbox"
|
||||
className="border-ink-300 bg-canvas-0 dark:border-ink-500 text-primary-600 focus:ring-primary-500 h-4 w-4 rounded hover:bg-canvas-200"
|
||||
checked={on}
|
||||
onChange={(e) => setYourFilters(e.target.checked)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setYourFilters(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor={label} className={clsx('text-ink-600 ml-2')}>
|
||||
{label}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {QuestionMarkCircleIcon} from '@heroicons/react/outline'
|
||||
import {QuestionMarkCircleIcon} from '@heroicons/react/24/outline'
|
||||
import {DisplayUser} from 'common/api/user-types'
|
||||
import {FilterFields} from 'common/filters'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {forwardRef, useEffect, useRef, useState} from 'react'
|
||||
import {forwardRef, ReactElement, useEffect, useRef, useState} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {IoFilterSharp} from 'react-icons/io5'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
@@ -116,7 +116,7 @@ export const Search = forwardRef<
|
||||
highlightFilters?: boolean
|
||||
highlightSort?: boolean
|
||||
setOpenFiltersModal?: (open: boolean) => void
|
||||
filtersElement: JSX.Element
|
||||
filtersElement: ReactElement
|
||||
}
|
||||
>((props, ref) => {
|
||||
const {
|
||||
@@ -218,7 +218,7 @@ export const Search = forwardRef<
|
||||
value={filters.name ?? ''}
|
||||
placeholder={placeholder}
|
||||
className={'w-full max-w-xs'}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateFilter({name: e.target.value})
|
||||
}}
|
||||
/>
|
||||
@@ -226,7 +226,7 @@ export const Search = forwardRef<
|
||||
<Row className="gap-2">
|
||||
<Select
|
||||
ref={sortSelectRef}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (isOrderBy(e.target.value)) {
|
||||
updateFilter({
|
||||
orderBy: e.target.value,
|
||||
|
||||
@@ -25,7 +25,9 @@ export function ShortBioToggle(props: {
|
||||
type="checkbox"
|
||||
className="border-ink-300 bg-canvas-0 dark:border-ink-500 text-primary-600 focus:ring-primary-500 h-4 w-4 rounded hover:bg-canvas-200 cursor-pointer"
|
||||
checked={on}
|
||||
onChange={(e) => updateFilter({shortBio: e.target.checked ? true : undefined})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
updateFilter({shortBio: e.target.checked ? true : undefined})
|
||||
}
|
||||
/>
|
||||
<label htmlFor={label} className={clsx('text-ink-600 ml-2')}>
|
||||
{label}
|
||||
|
||||
@@ -21,7 +21,9 @@ export function FontPicker(props: {className?: string} = {}) {
|
||||
<select
|
||||
id="font-picker"
|
||||
value={font}
|
||||
onChange={(e) => setFont(e.target.value as FontOption)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setFont(e.target.value as FontOption)
|
||||
}
|
||||
className={clsx(
|
||||
'rounded-md border border-gray-300 px-2 py-1 text-sm bg-canvas-50',
|
||||
className,
|
||||
|
||||
@@ -12,7 +12,7 @@ export function LanguagePicker(props: {className?: string} = {}) {
|
||||
<select
|
||||
id="locale-picker"
|
||||
value={locale}
|
||||
onChange={(e) => setLocale(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setLocale(e.target.value)}
|
||||
className={clsx(
|
||||
'rounded-md border border-gray-300 px-2 py-1 text-sm bg-canvas-50',
|
||||
className,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx'
|
||||
import {forwardRef} from 'react'
|
||||
import {ComponentPropsWithoutRef, forwardRef} from 'react'
|
||||
|
||||
export const Col = forwardRef(function Col(
|
||||
props: JSX.IntrinsicElements['div'],
|
||||
props: ComponentPropsWithoutRef<'div'>,
|
||||
ref: React.Ref<HTMLDivElement>,
|
||||
) {
|
||||
const {children, className, ...rest} = props
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Dialog, Transition} from '@headlessui/react'
|
||||
import {XIcon} from '@heroicons/react/outline'
|
||||
import {XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Fragment, ReactNode, useEffect, useRef} from 'react'
|
||||
|
||||
@@ -94,7 +94,7 @@ export function Modal(props: {
|
||||
position === 'top' && 'sm:-bottom-4 sm:top-auto sm:translate-y-full',
|
||||
)}
|
||||
>
|
||||
<XIcon className="h-8 w-8" />
|
||||
<XMarkIcon className="h-8 w-8" />
|
||||
<div className="sr-only">Close</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx'
|
||||
import {forwardRef} from 'react'
|
||||
import {ComponentPropsWithoutRef, forwardRef} from 'react'
|
||||
|
||||
export const Row = forwardRef(function Row(
|
||||
props: JSX.IntrinsicElements['div'],
|
||||
props: ComponentPropsWithoutRef<'div'>,
|
||||
ref: React.Ref<HTMLDivElement>,
|
||||
) {
|
||||
const {children, className, ...rest} = props
|
||||
|
||||
@@ -48,7 +48,7 @@ export function MinimalistTabs(props: TabProps & {activeIndex: number}) {
|
||||
<a
|
||||
href="#"
|
||||
key={tab.queryString ?? tab.title}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
onClick?.(tab.queryString?.toLowerCase() ?? tab.title.toLowerCase(), i)
|
||||
if (trackingName) {
|
||||
@@ -108,7 +108,7 @@ export function ControlledTabs(props: TabProps & {activeIndex: number}) {
|
||||
<a
|
||||
href="#"
|
||||
key={tab.queryString ?? tab.title}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
onClick?.(tab.queryString?.toLowerCase() ?? tab.title.toLowerCase(), i)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {HeartIcon, UserIcon} from '@heroicons/react/solid'
|
||||
import {HeartIcon, UserIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import Image from 'next/image'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PlusIcon} from '@heroicons/react/solid'
|
||||
import {PlusIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {DisplayUser} from 'common/api/user-types'
|
||||
import {APIError} from 'common/api/utils'
|
||||
|
||||
@@ -118,7 +118,7 @@ export const MultiCheckbox = (props: {
|
||||
<Input
|
||||
value={newLabel}
|
||||
placeholder={addPlaceholder ?? t('multi-checkbox.search_or_add', 'Search or add')}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setNewLabel(e.target.value)
|
||||
setError(null)
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Dialog, Transition} from '@headlessui/react'
|
||||
import {MenuAlt3Icon} from '@heroicons/react/solid'
|
||||
import {MenuAlt3Icon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {User} from 'common/user'
|
||||
import Link from 'next/link'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {LoginIcon, LogoutIcon} from '@heroicons/react/outline'
|
||||
import {LoginIcon, LogoutIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {ANDROID_APP_URL} from 'common/constants'
|
||||
import {buildArray} from 'common/util/array'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {SparklesIcon} from '@heroicons/react/solid'
|
||||
import {SparklesIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {ENV_CONFIG} from 'common/envs/constants'
|
||||
import {Notification} from 'common/notifications'
|
||||
@@ -297,7 +297,7 @@ export function AvatarNotificationIcon(props: {
|
||||
<Link
|
||||
href={href}
|
||||
target={href.startsWith('http') ? '_blank' : undefined}
|
||||
onClick={(e) => e.stopPropagation}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation}
|
||||
>
|
||||
<Avatar
|
||||
username={sourceUserName}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import {BellIcon} from '@heroicons/react/outline'
|
||||
import {BellIcon as SolidBellIcon} from '@heroicons/react/solid'
|
||||
import {BellIcon} from '@heroicons/react/24/outline'
|
||||
import {BellIcon as SolidBellIcon} from '@heroicons/react/24/solid'
|
||||
import {NOTIFICATIONS_PER_PAGE} from 'common/notifications'
|
||||
import {PrivateUser} from 'common/user'
|
||||
import {usePathname} from 'next/navigation'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {PlusIcon, XIcon} from '@heroicons/react/solid'
|
||||
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid'
|
||||
import {Editor} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
@@ -353,7 +353,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
min={18}
|
||||
max={100}
|
||||
error={!!ageError}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value ? Number(e.target.value) : null
|
||||
if (value !== null && value < 18) {
|
||||
setAgeError(
|
||||
@@ -378,7 +378,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
type="number"
|
||||
data-testid="height-feet"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (e.target.value === '') {
|
||||
setHeightFeet(undefined)
|
||||
} else {
|
||||
@@ -397,7 +397,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
type="number"
|
||||
data-testid="height-inches"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (e.target.value === '') {
|
||||
setHeightInches(undefined)
|
||||
} else {
|
||||
@@ -419,7 +419,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
type="number"
|
||||
data-testid="height-centimeters"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (e.target.value === '') {
|
||||
setHeightFeet(undefined)
|
||||
setHeightInches(undefined)
|
||||
@@ -512,7 +512,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
</label>
|
||||
<Textarea
|
||||
data-testid="headline"
|
||||
onChange={(e) => setProfile('headline', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('headline', e.target.value)
|
||||
}
|
||||
className={'w-full md:w-[700px] bg-canvas-0 border rounded-md p-2'}
|
||||
value={profile['headline'] ?? undefined}
|
||||
maxLength={250}
|
||||
@@ -532,7 +534,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="keywords"
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setKeywordsString(e.target.value)
|
||||
const keywords = e.target.value
|
||||
.split(',')
|
||||
@@ -577,7 +579,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Select
|
||||
data-testid="pref-age-min"
|
||||
value={profile['pref_age_min'] ?? ''}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newMin = e.target.value ? Number(e.target.value) : 18
|
||||
const currentMax = profile['pref_age_max'] ?? 100
|
||||
setProfile('pref_age_min', Math.min(newMin, currentMax))
|
||||
@@ -597,7 +599,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Select
|
||||
data-testid="pref-age-max"
|
||||
value={profile['pref_age_max'] ?? ''}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newMax = e.target.value ? Number(e.target.value) : 100
|
||||
const currentMin = profile['pref_age_min'] ?? 18
|
||||
setProfile('pref_age_max', Math.max(newMax, currentMin))
|
||||
@@ -669,7 +671,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="current-number-of-kids"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value === '' ? null : Number(e.target.value)
|
||||
setProfile('has_kids', value)
|
||||
}}
|
||||
@@ -747,7 +749,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="university"
|
||||
type="text"
|
||||
onChange={(e) => setProfile('university', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('university', e.target.value)
|
||||
}
|
||||
className={'w-52'}
|
||||
value={profile['university'] ?? undefined}
|
||||
/>
|
||||
@@ -766,7 +770,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="job-title"
|
||||
type="text"
|
||||
onChange={(e) => setProfile('occupation_title', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('occupation_title', e.target.value)
|
||||
}
|
||||
className={'w-52'}
|
||||
value={profile['occupation_title'] ?? undefined}
|
||||
/>
|
||||
@@ -777,7 +783,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="company"
|
||||
type="text"
|
||||
onChange={(e) => setProfile('company', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('company', e.target.value)
|
||||
}
|
||||
className={'w-52'}
|
||||
value={profile['company'] ?? undefined}
|
||||
/>
|
||||
@@ -808,7 +816,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="political-belief-details"
|
||||
type="text"
|
||||
onChange={(e) => setProfile('political_details', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('political_details', e.target.value)
|
||||
}
|
||||
className={'w-full sm:w-96'}
|
||||
value={profile['political_details'] ?? undefined}
|
||||
/>
|
||||
@@ -830,7 +840,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="religious-belief-details"
|
||||
type="text"
|
||||
onChange={(e) => setProfile('religious_beliefs', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setProfile('religious_beliefs', e.target.value)
|
||||
}
|
||||
className={'w-full sm:w-96'}
|
||||
value={profile['religious_beliefs'] ?? undefined}
|
||||
/>
|
||||
@@ -928,7 +940,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
data-testid="alcohol-consumed-per-month"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value === '' ? null : Number(e.target.value)
|
||||
setProfile('drinks_per_month', value)
|
||||
}}
|
||||
@@ -962,7 +974,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
<label className={clsx(labelClassName)}>Birthplace</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfileState('born_in_location', e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setProfileState('born_in_location', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['born_in_location'] ?? undefined}
|
||||
/>
|
||||
@@ -996,11 +1008,13 @@ export const OptionalProfileUserForm = (props: {
|
||||
<Input
|
||||
type="text"
|
||||
value={value!}
|
||||
onChange={(e) => updateUserLink(platform, e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
updateUserLink(platform, e.target.value)
|
||||
}
|
||||
className="col-span-2 sm:col-span-1"
|
||||
/>
|
||||
<IconButton onClick={() => updateUserLink(platform, null)}>
|
||||
<XIcon className="h-6 w-6" />
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
<div className="sr-only">{t('common.remove', 'Remove')}</div>
|
||||
</IconButton>
|
||||
</Fragment>
|
||||
@@ -1022,7 +1036,9 @@ export const OptionalProfileUserForm = (props: {
|
||||
: t('profile.optional.site_url', 'Site URL')
|
||||
}
|
||||
value={newLinkValue}
|
||||
onChange={(e) => setNewLinkValue(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setNewLinkValue(e.target.value)
|
||||
}
|
||||
// disable password managers
|
||||
autoComplete="off"
|
||||
data-1p-ignore
|
||||
@@ -1118,7 +1134,7 @@ const CitySearchBox = (props: {onCitySelected: (city: City | undefined) => void}
|
||||
<>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setQuery(e.target.value)}
|
||||
placeholder={t('profile.optional.search_city', 'Search city...')}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={(e) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
HomeIcon,
|
||||
NewspaperIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from '@heroicons/react/outline'
|
||||
} from '@heroicons/react/24/outline'
|
||||
import {
|
||||
CogIcon,
|
||||
GlobeAltIcon,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
QuestionMarkCircleIcon as SolidQuestionIcon,
|
||||
UserCircleIcon,
|
||||
UsersIcon,
|
||||
} from '@heroicons/react/solid'
|
||||
} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {IS_MAINTENANCE} from 'common/constants'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ClockIcon} from '@heroicons/react/solid'
|
||||
import {ClockIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
INVERTED_DIET_CHOICES,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {EyeOffIcon, FlagIcon} from '@heroicons/react/outline'
|
||||
import {DotsHorizontalIcon, ReplyIcon} from '@heroicons/react/solid'
|
||||
import {EyeSlashIcon, FlagIcon} from '@heroicons/react/24/outline'
|
||||
import {EllipsisHorizontalIcon, ReplyIcon} from '@heroicons/react/24/solid'
|
||||
import {Editor} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {type Comment, MAX_COMMENT_LENGTH, ReplyToUserInfo} from 'common/comment'
|
||||
@@ -273,7 +273,7 @@ function DotMenu(props: {onUser: User; comment: Comment; onHide: () => void}) {
|
||||
<DropdownMenu
|
||||
menuWidth={'w-36'}
|
||||
closeOnClick={true}
|
||||
icon={<DotsHorizontalIcon className="mt-[0.12rem] h-4 w-4" aria-hidden="true" />}
|
||||
icon={<EllipsisHorizontalIcon className="mt-[0.12rem] h-4 w-4" aria-hidden="true" />}
|
||||
items={buildArray(
|
||||
user &&
|
||||
comment.userId !== user.id && {
|
||||
@@ -286,7 +286,7 @@ function DotMenu(props: {onUser: User; comment: Comment; onHide: () => void}) {
|
||||
},
|
||||
(isAdmin || isCurrentUser || isOwner) && {
|
||||
name: comment.hidden ? 'Undelete' : 'Delete',
|
||||
icon: <EyeOffIcon className="h-5 w-5 text-red-500" />,
|
||||
icon: <EyeSlashIcon className="h-5 w-5 text-red-500" />,
|
||||
onClick: async () => {
|
||||
onHide()
|
||||
await toast.promise(
|
||||
@@ -326,7 +326,7 @@ function CommentActions(props: {
|
||||
<Tooltip text="Reply" placement="bottom">
|
||||
<IconButton
|
||||
size={'xs'}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onReplyClick(comment)
|
||||
|
||||
@@ -123,7 +123,7 @@ function ProfilePreview(props: {
|
||||
</span>
|
||||
<button
|
||||
className="text-primary-500 hover:text-primary-700 underline"
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onUndoHidden?.(profile.user_id)
|
||||
|
||||
@@ -202,7 +202,9 @@ export function DeleteAccountSurveyModal() {
|
||||
className="block w-full bg-canvas-0 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||
placeholder={t('delete_survey.other_placeholder', 'Please share more details')}
|
||||
value={reasonFreeText}
|
||||
onChange={(e) => setReasonFreeText(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setReasonFreeText(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import {DotsHorizontalIcon, EyeIcon, LockClosedIcon, PencilIcon} from '@heroicons/react/outline'
|
||||
import {
|
||||
EllipsisHorizontalIcon,
|
||||
EyeIcon,
|
||||
LockClosedIcon,
|
||||
PencilIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {debug} from 'common/logger'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
@@ -142,7 +147,7 @@ export default function ProfileHeader(props: {
|
||||
>
|
||||
<DropdownMenu
|
||||
menuWidth={'w-52'}
|
||||
icon={<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />}
|
||||
icon={<EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />}
|
||||
items={[
|
||||
{
|
||||
name:
|
||||
|
||||
@@ -93,7 +93,9 @@ export const ReportUser = (props: {user: User; closeModal: () => void}) => {
|
||||
rows={2}
|
||||
className={'border-ink-300 bg-canvas-0 -ml-2 rounded-md border p-2'}
|
||||
value={otherReportType}
|
||||
onChange={(e) => setOtherReportType(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setOtherReportType(e.target.value)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Row className={'justify-between'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {EyeIcon, LockClosedIcon} from '@heroicons/react/outline'
|
||||
import {EyeIcon, LockClosedIcon} from '@heroicons/react/24/outline'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Modal} from 'web/components/layout/modal'
|
||||
|
||||
@@ -127,7 +127,9 @@ const QuestionRow = (props: {row: rowFor<'compatibility_prompts'>; user: User})
|
||||
className={'w-full max-w-xl'}
|
||||
rows={3}
|
||||
value={form.free_response ?? ''}
|
||||
onChange={(e) => setForm({...form, free_response: e.target.value})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setForm({...form, free_response: e.target.value})
|
||||
}
|
||||
onBlur={() => submitAnswer(form)}
|
||||
/>
|
||||
) : answer_type === 'multiple_choice' && row.multiple_choice_options ? (
|
||||
@@ -148,7 +150,9 @@ const QuestionRow = (props: {row: rowFor<'compatibility_prompts'>; user: User})
|
||||
className={'w-20'}
|
||||
max={1000}
|
||||
min={0}
|
||||
onChange={(e) => setForm({...form, integer: Number(e.target.value)})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setForm({...form, integer: Number(e.target.value)})
|
||||
}
|
||||
value={form.integer ?? undefined}
|
||||
onBlur={() => submitAnswer(form)}
|
||||
/>
|
||||
@@ -188,7 +192,9 @@ export const IndividualQuestionRow = (props: {
|
||||
className={'w-full'}
|
||||
rows={3}
|
||||
value={form.free_response ?? ''}
|
||||
onChange={(e) => setForm({...form, free_response: e.target.value})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setForm({...form, free_response: e.target.value})
|
||||
}
|
||||
/>
|
||||
) : answer_type === 'multiple_choice' && row.multiple_choice_options ? (
|
||||
<RadioToggleGroup
|
||||
@@ -205,7 +211,9 @@ export const IndividualQuestionRow = (props: {
|
||||
className={'w-20'}
|
||||
max={1000}
|
||||
min={0}
|
||||
onChange={(e) => setForm({...form, integer: Number(e.target.value)})}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setForm({...form, integer: Number(e.target.value)})
|
||||
}
|
||||
value={form.integer ?? undefined}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const RequiredProfileUserForm = (props: {
|
||||
type="text"
|
||||
placeholder="Display name"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateUserState({name: e.target.value || ''})
|
||||
}}
|
||||
onBlur={updateDisplayName}
|
||||
@@ -132,7 +132,7 @@ export const RequiredProfileUserForm = (props: {
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateUserState({
|
||||
username: e.target.value || '',
|
||||
errorUsername: '',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {XIcon} from '@heroicons/react/outline'
|
||||
import {XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {DisplayUser} from 'common/api/user-types'
|
||||
import {FilterFields} from 'common/filters'
|
||||
@@ -108,7 +108,7 @@ function ButtonModal(props: {
|
||||
}}
|
||||
className="inline-flex items-center justify-center h-8 w-8 rounded-full text-red-600 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
|
||||
>
|
||||
<XIcon className="h-6 w-6" />
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</Row>
|
||||
))}
|
||||
@@ -241,7 +241,7 @@ function StarModal(props: {
|
||||
}}
|
||||
className="inline-flex items-center justify-center h-8 w-8 rounded-full text-red-600 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
|
||||
>
|
||||
<XIcon className="h-6 w-6" />
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Menu, Transition} from '@headlessui/react'
|
||||
import {XIcon} from '@heroicons/react/outline'
|
||||
import {XMarkIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Fragment, useEffect, useRef, useState} from 'react'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
@@ -82,7 +82,7 @@ export function SelectUsers(props: {
|
||||
name="user name"
|
||||
id="user name"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setQuery(e.target.value)}
|
||||
placeholder="Search users..."
|
||||
/>
|
||||
</Col>
|
||||
@@ -160,7 +160,7 @@ export function SelectUsers(props: {
|
||||
color={'gray-white'}
|
||||
size={'xs'}
|
||||
>
|
||||
<XIcon className="h-5 w-5" aria-hidden="true" />
|
||||
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Button>
|
||||
</Row>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MoonIcon, SunIcon} from '@heroicons/react/outline'
|
||||
import {MoonIcon, SunIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {useTheme} from 'web/hooks/use-theme'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DocumentReportIcon, LinkIcon} from '@heroicons/react/solid'
|
||||
import {DocumentReportIcon, LinkIcon} from '@heroicons/react/24/solid'
|
||||
import {Site} from 'common/socials'
|
||||
import {ReactNode} from 'react'
|
||||
import {LuBookmark, LuHandshake, LuHeart, LuUsers} from 'react-icons/lu'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {PrivateUser} from 'common/user'
|
||||
import {ReactElement} from 'react'
|
||||
import {CompassLoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {usePrivateUser} from 'web/hooks/use-user'
|
||||
|
||||
export function WithPrivateUser({children}: {children: (user: PrivateUser) => JSX.Element}) {
|
||||
export function WithPrivateUser({children}: {children: (user: PrivateUser) => ReactElement}) {
|
||||
const privateUser = usePrivateUser()
|
||||
if (!privateUser) return <CompassLoadingIndicator />
|
||||
return children(privateUser)
|
||||
|
||||
@@ -46,7 +46,9 @@ export function VoteComponent() {
|
||||
<select
|
||||
id="orderBy"
|
||||
value={orderBy}
|
||||
onChange={(e) => setOrderBy(e.target.value as OrderBy)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setOrderBy(e.target.value as OrderBy)
|
||||
}
|
||||
className="rounded-md border border-gray-300 px-2 py-1 text-sm bg-canvas-50"
|
||||
>
|
||||
{ORDER_BY.map((key) => (
|
||||
@@ -77,7 +79,7 @@ export function VoteComponent() {
|
||||
value={title}
|
||||
placeholder={t('vote.form.title_placeholder', 'Title')}
|
||||
className={'w-full mb-2'}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTitle(e.target.value)
|
||||
}}
|
||||
/>
|
||||
@@ -87,7 +89,9 @@ export function VoteComponent() {
|
||||
type="checkbox"
|
||||
id="anonymous"
|
||||
checked={isAnonymous}
|
||||
onChange={(e) => setIsAnonymous(e.target.checked)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setIsAnonymous(e.target.checked)
|
||||
}
|
||||
className="h-4 w-4 rounded-md border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="anonymous">{t('vote.form.anonymous', 'Anonymous?')}</label>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {CheckCircleIcon} from '@heroicons/react/outline'
|
||||
import {PlusIcon, XIcon} from '@heroicons/react/solid'
|
||||
import {CheckCircleIcon} from '@heroicons/react/24/outline'
|
||||
import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {User} from 'common/user'
|
||||
import {buildArray} from 'common/util/array'
|
||||
@@ -101,7 +101,7 @@ export const AddPhotosWidget = (props: {
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
const newUrls = (photo_urls ?? []).filter((u) => u !== url)
|
||||
if (isPinned) setPinnedUrl(newUrls[0] ?? '')
|
||||
@@ -111,7 +111,7 @@ export const AddPhotosWidget = (props: {
|
||||
size={'2xs'}
|
||||
className={clsx('bg-canvas-0 absolute right-0 top-0 !rounded-full !px-1 py-1')}
|
||||
>
|
||||
<XIcon className={'h-4 w-4'} />
|
||||
<XMarkIcon className={'h-4 w-4'} />
|
||||
</Button>
|
||||
<Image
|
||||
src={url}
|
||||
@@ -123,11 +123,11 @@ export const AddPhotosWidget = (props: {
|
||||
|
||||
<textarea
|
||||
// stop click bubbling so clicking/focusing the input doesn't pin the image
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
|
||||
aria-label={`description for image ${index}`}
|
||||
placeholder={t('add_photos.add_description', 'Add description')}
|
||||
value={image_descriptions?.[url] ?? ''}
|
||||
onChange={(e) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
e.stopPropagation()
|
||||
const v = e.target.value
|
||||
setDescription(url, v)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ExclamationIcon} from '@heroicons/react/solid'
|
||||
import {ExclamationTriangleIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {ReactNode} from 'react'
|
||||
|
||||
@@ -10,7 +10,7 @@ export function AlertBox(props: {title: string; className?: string; children?: R
|
||||
return (
|
||||
<Col className={clsx('border-warning bg-warning/10 w-full rounded-md border p-4', className)}>
|
||||
<Row className="flex-shrink-0">
|
||||
<ExclamationIcon className="fill-warning h-5 w-5" aria-hidden />
|
||||
<ExclamationTriangleIcon className="fill-warning h-5 w-5" aria-hidden />
|
||||
|
||||
<div className="ml-3">
|
||||
<h3 className="text-ink-800 text-sm font-medium">{title}</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {UserIcon, UsersIcon} from '@heroicons/react/solid'
|
||||
import {UserIcon, UsersIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {floor} from 'lodash'
|
||||
import Image from 'next/image'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from 'clsx'
|
||||
import {forwardRef} from 'react'
|
||||
import {ComponentPropsWithoutRef, forwardRef} from 'react'
|
||||
|
||||
export const Card = forwardRef(function Card(
|
||||
props: JSX.IntrinsicElements['div'],
|
||||
props: ComponentPropsWithoutRef<'div'>,
|
||||
ref: React.Ref<HTMLDivElement>,
|
||||
) {
|
||||
const {children, className, ...rest} = props
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/solid'
|
||||
import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {throttle} from 'lodash'
|
||||
import {forwardRef, ReactNode, Ref, useEffect, useRef, useState} from 'react'
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Checkbox(props: {
|
||||
type="checkbox"
|
||||
className="border-ink-300 bg-canvas-0 dark:border-ink-500 text-primary-600 focus:ring-primary-500 h-5 w-5 rounded hover:bg-canvas-300"
|
||||
checked={checked}
|
||||
onChange={(e) => toggle(e.target.checked)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => toggle(e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ClickFrame = forwardRef(
|
||||
// we put pointer-events:auto on links, buttons, and elements with class stop-prop,
|
||||
// so they get caught by the stopPropagation below
|
||||
className="pointer-events-none contents [&_.stop-prop]:pointer-events-auto [&_a]:pointer-events-auto [&_button]:pointer-events-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/solid'
|
||||
import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/24/solid'
|
||||
import {JSONContent} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {MouseEventHandler, useEffect, useRef, useState} from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {BadgeCheckIcon} from '@heroicons/react/solid'
|
||||
import {CheckBadgeIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
||||
import {formatPercent} from 'common/util/format'
|
||||
@@ -12,7 +12,7 @@ export const CompatibleBadge = (props: {compatibility: CompatibilityScore; class
|
||||
return (
|
||||
<Tooltip text={t('compatibility.tooltip', 'Compatibility score between you two')}>
|
||||
<Row className={clsx('items-center gap-1 text-sm font-semibold', className)}>
|
||||
<BadgeCheckIcon className="h-4 w-4" />
|
||||
<CheckBadgeIcon className="h-4 w-4" />
|
||||
{formatPercent(compatibility.score ?? 0)}{' '}
|
||||
</Row>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {Popover} from '@headlessui/react'
|
||||
import {flip, offset, shift, useFloating} from '@floating-ui/react'
|
||||
import {Popover, PopoverButton, PopoverPanel} from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import {useState} from 'react'
|
||||
import {usePopper} from 'react-popper'
|
||||
import {NewBadge} from 'web/components/new-badge'
|
||||
|
||||
import {AnimationOrNothing} from '../comments/dropdown-menu'
|
||||
@@ -16,9 +15,7 @@ export function CustomizeableDropdown(props: {
|
||||
closeOnClick?: boolean
|
||||
withinOverflowContainer?: boolean
|
||||
popoverClassName?: string
|
||||
// When true, shows a tiny "new" badge at the top-left of the button
|
||||
showNewBadge?: boolean
|
||||
// Optional extra classes for the badge container (to tweak position/size)
|
||||
newBadgeClassName?: string
|
||||
}) {
|
||||
const {
|
||||
@@ -33,32 +30,30 @@ export function CustomizeableDropdown(props: {
|
||||
showNewBadge,
|
||||
newBadgeClassName,
|
||||
} = props
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>()
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>()
|
||||
const {styles, attributes} = usePopper(referenceElement, popperElement, {
|
||||
|
||||
const {refs, floatingStyles} = useFloating({
|
||||
strategy: withinOverflowContainer ? 'fixed' : 'absolute',
|
||||
middleware: [offset(8), flip(), shift({padding: 8})],
|
||||
})
|
||||
|
||||
return (
|
||||
<Popover className={clsx('relative inline-block text-left', className)}>
|
||||
{({open, close}) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
ref={setReferenceElement}
|
||||
<PopoverButton
|
||||
ref={refs.setReference}
|
||||
className={clsx('flex items-center relative hover-bold', buttonClass)}
|
||||
onClick={(e: any) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
{showNewBadge && <NewBadge classes={newBadgeClassName} />}
|
||||
{buttonContent(open)}
|
||||
</Popover.Button>
|
||||
</PopoverButton>
|
||||
|
||||
<AnimationOrNothing show={open} animate={!withinOverflowContainer}>
|
||||
<Popover.Panel
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
<PopoverPanel
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
className={clsx(
|
||||
'bg-canvas-0 ring-ink-1000 z-30 rounded-md px-2 py-2 shadow-lg ring-1 ring-opacity-5 focus:outline-none',
|
||||
menuWidth ?? 'w-36',
|
||||
@@ -68,7 +63,7 @@ export function CustomizeableDropdown(props: {
|
||||
{typeof dropdownMenuContent === 'function'
|
||||
? dropdownMenuContent(close)
|
||||
: dropdownMenuContent}
|
||||
</Popover.Panel>
|
||||
</PopoverPanel>
|
||||
</AnimationOrNothing>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const EditInPlaceInput = (props: {
|
||||
<ExpandingInput
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}
|
||||
onBlur={save}
|
||||
onKeyDown={(e) => e.key === 'Enter' && save()}
|
||||
autoFocus
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {XIcon} from '@heroicons/react/solid'
|
||||
import {XMarkIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import Image from 'next/image'
|
||||
import {useState} from 'react'
|
||||
@@ -165,13 +165,13 @@ const PhotoItem = ({
|
||||
/>
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onDelete(url)
|
||||
}}
|
||||
className="bg-canvas-0 hover:bg-ink-100 absolute right-2 top-2 rounded-full p-1"
|
||||
>
|
||||
<XIcon className="h-5 w-5" />
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {DocumentTextIcon} from '@heroicons/react/outline'
|
||||
import {DocumentTextIcon} from '@heroicons/react/24/outline'
|
||||
import {JSONContent} from '@tiptap/react'
|
||||
import clsx from 'clsx'
|
||||
import {MouseEventHandler, ReactNode, useRef, useState} from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {EyeIcon, EyeOffIcon} from '@heroicons/react/outline'
|
||||
import {EyeIcon, EyeSlashIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {useMemo, useState} from 'react'
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
@@ -88,7 +88,7 @@ export function HideProfileButton(props: HideProfileButtonProps) {
|
||||
}
|
||||
>
|
||||
{hidden || eyeOff ? (
|
||||
<EyeOffIcon className={clsx('h-5 w-5 guidance', iconClassName)} />
|
||||
<EyeSlashIcon className={clsx('h-5 w-5 guidance', iconClassName)} />
|
||||
) : (
|
||||
<EyeIcon className={clsx('h-5 w-5 guidance', iconClassName)} />
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {InformationCircleIcon} from '@heroicons/react/solid'
|
||||
import {InformationCircleIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {ReactNode} from 'react'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Placement} from '@floating-ui/react'
|
||||
import {InformationCircleIcon} from '@heroicons/react/outline'
|
||||
import {InformationCircleIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import {Tooltip} from './tooltip'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import clsx from 'clsx'
|
||||
import {forwardRef, Ref} from 'react'
|
||||
import {ComponentPropsWithoutRef, forwardRef, Ref} from 'react'
|
||||
|
||||
/** Text input. Wraps html `<input>` */
|
||||
export const Input = forwardRef(
|
||||
(
|
||||
props: {
|
||||
error?: boolean
|
||||
} & JSX.IntrinsicElements['input'],
|
||||
} & ComponentPropsWithoutRef<'input'>,
|
||||
ref: Ref<HTMLInputElement>,
|
||||
) => {
|
||||
const {error, className, ...rest} = props
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {HeartIcon} from '@heroicons/react/outline'
|
||||
import {HeartIcon} from '@heroicons/react/24/outline'
|
||||
import clsx from 'clsx'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {useState} from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {UserIcon} from '@heroicons/react/solid'
|
||||
import {UserIcon} from '@heroicons/react/24/solid'
|
||||
import {LikeData, ShipData} from 'common/api/profile-types'
|
||||
import {Profile} from 'common/profiles/profile'
|
||||
import {keyBy, orderBy} from 'lodash'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/solid'
|
||||
import {ChevronLeftIcon, ChevronRightIcon} from '@heroicons/react/24/solid'
|
||||
import clsx from 'clsx'
|
||||
import {range} from 'lodash'
|
||||
import {usePathname, useRouter} from 'next/navigation'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user