mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-11 18:17:36 -04:00
Add profile fields: work areas, moral causes, and interests
This commit is contained in:
@@ -73,6 +73,8 @@ import {IS_LOCAL} from "common/hosting/constants";
|
||||
import {editMessage} from "api/edit-message";
|
||||
import {reactToMessage} from "api/react-to-message";
|
||||
import {deleteMessage} from "api/delete-message";
|
||||
import {updateOptions} from "api/update-options";
|
||||
import {getOptions} from "api/get-options";
|
||||
|
||||
// const corsOptions: CorsOptions = {
|
||||
// origin: ['*'], // Only allow requests from this domain
|
||||
@@ -366,6 +368,8 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
'delete-message': deleteMessage,
|
||||
'edit-message': editMessage,
|
||||
'react-to-message': reactToMessage,
|
||||
'update-options': updateOptions,
|
||||
'get-options': getOptions,
|
||||
// 'auth-google': authGoogle,
|
||||
}
|
||||
|
||||
|
||||
28
backend/api/src/get-options.ts
Normal file
28
backend/api/src/get-options.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {APIError, APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {log} from 'shared/utils'
|
||||
import {tryCatch} from 'common/util/try-catch'
|
||||
import {OPTION_TABLES} from "common/profiles/constants";
|
||||
|
||||
export const getOptions: APIHandler<'get-options'> = async (
|
||||
{table},
|
||||
_auth
|
||||
) => {
|
||||
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
const result = await tryCatch(
|
||||
pg.manyOrNone<{ name: string }>(`SELECT interests.name
|
||||
FROM interests`)
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
log('Error getting profile options', result.error)
|
||||
throw new APIError(500, 'Error getting profile options')
|
||||
}
|
||||
|
||||
const names = result.data.map(row => row.name)
|
||||
return {names}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {createSupabaseDirectClient, pgp} from 'shared/supabase/init'
|
||||
import {from, join, leftJoin, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
|
||||
import {MIN_BIO_LENGTH} from "common/constants";
|
||||
import {compact} from "lodash";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
export type profileQueryType = {
|
||||
limit?: number | undefined,
|
||||
@@ -37,6 +38,8 @@ export type profileQueryType = {
|
||||
skipId?: string | undefined,
|
||||
orderBy?: string | undefined,
|
||||
lastModificationWithin?: string | undefined,
|
||||
} & {
|
||||
[K in OptionTableKey]?: string[] | undefined
|
||||
}
|
||||
|
||||
// const userActivityColumns = ['last_online_time']
|
||||
@@ -44,7 +47,7 @@ export type profileQueryType = {
|
||||
|
||||
export const loadProfiles = async (props: profileQueryType) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
console.debug(props)
|
||||
console.debug('loadProfiles', props)
|
||||
const {
|
||||
limit: limitParam,
|
||||
after,
|
||||
@@ -66,6 +69,9 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
religion,
|
||||
wants_kids_strength,
|
||||
has_kids,
|
||||
interests,
|
||||
causes,
|
||||
work,
|
||||
is_smoker,
|
||||
shortBio,
|
||||
geodbCityIds,
|
||||
@@ -95,24 +101,55 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
: 'profiles'
|
||||
|
||||
const userActivityJoin = 'user_activity on user_activity.user_id = profiles.user_id'
|
||||
|
||||
// Pre-aggregated interests per profile
|
||||
function getManyToManyJoin(label: OptionTableKey) {
|
||||
return `(
|
||||
SELECT
|
||||
profile_${label}.profile_id,
|
||||
ARRAY_AGG(${label}.name ORDER BY ${label}.name) AS ${label}
|
||||
FROM profile_${label}
|
||||
JOIN ${label} ON ${label}.id = profile_${label}.option_id
|
||||
GROUP BY profile_${label}.profile_id
|
||||
) i ON i.profile_id = profiles.id`
|
||||
}
|
||||
const interestsJoin = getManyToManyJoin('interests')
|
||||
const causesJoin = getManyToManyJoin('causes')
|
||||
const workJoin = getManyToManyJoin('work')
|
||||
|
||||
const compatibilityScoreJoin = pgp.as.format(`compatibility_scores cs on (cs.user_id_1 = LEAST(profiles.user_id, $(compatibleWithUserId)) and cs.user_id_2 = GREATEST(profiles.user_id, $(compatibleWithUserId)))`, {compatibleWithUserId})
|
||||
|
||||
const joins = [
|
||||
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
|
||||
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
|
||||
interests && leftJoin(interestsJoin),
|
||||
causes && leftJoin(causesJoin),
|
||||
work && leftJoin(workJoin),
|
||||
]
|
||||
|
||||
const _orderBy = orderByParam === 'compatibility_score' ? 'cs.score' : `${tablePrefix}.${orderByParam}`
|
||||
const afterFilter = renderSql(
|
||||
select(_orderBy),
|
||||
from('profiles'),
|
||||
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
|
||||
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
|
||||
...joins,
|
||||
where('profiles.id = $(after)', {after}),
|
||||
)
|
||||
|
||||
const tableSelection = compact([
|
||||
from('profiles'),
|
||||
join('users on users.id = profiles.user_id'),
|
||||
orderByParam === 'last_online_time' && leftJoin(userActivityJoin),
|
||||
orderByParam === 'compatibility_score' && compatibleWithUserId && join(compatibilityScoreJoin),
|
||||
...joins,
|
||||
])
|
||||
|
||||
function getManyToManyClause(label: OptionTableKey) {
|
||||
return `EXISTS (
|
||||
SELECT 1 FROM profile_${label} pi2
|
||||
JOIN ${label} ii2 ON ii2.id = pi2.option_id
|
||||
WHERE pi2.profile_id = profiles.id
|
||||
AND ii2.name = ANY (ARRAY[$(values)])
|
||||
)`
|
||||
}
|
||||
|
||||
const filters = [
|
||||
where('looking_for_matches = true'),
|
||||
where(`profiles.disabled != true`),
|
||||
@@ -190,6 +227,12 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
{religion}
|
||||
),
|
||||
|
||||
interests?.length && where(getManyToManyClause('interests'), {values: interests}),
|
||||
|
||||
causes?.length && where(getManyToManyClause('causes'), {values: causes}),
|
||||
|
||||
work?.length && where(getManyToManyClause('work'), {values: work}),
|
||||
|
||||
!!wants_kids_strength &&
|
||||
wants_kids_strength !== -1 &&
|
||||
where(
|
||||
@@ -229,12 +272,15 @@ export const loadProfiles = async (props: profileQueryType) => {
|
||||
lastModificationWithin && where(`last_modification_time >= NOW() - INTERVAL $(lastModificationWithin)`, {lastModificationWithin}),
|
||||
]
|
||||
|
||||
let selectCols = 'profiles.*, name, username, users.data as user'
|
||||
let selectCols = 'profiles.*, users.name, users.username, users.data as user'
|
||||
if (orderByParam === 'compatibility_score') {
|
||||
selectCols += ', cs.score as compatibility_score'
|
||||
} else if (orderByParam === 'last_online_time') {
|
||||
selectCols += ', user_activity.last_online_time'
|
||||
}
|
||||
if (interests) selectCols += `, COALESCE(i.interests, '{}') AS interests`
|
||||
if (causes) selectCols += `, COALESCE(i.causes, '{}') AS causes`
|
||||
if (work) selectCols += `, COALESCE(i.work, '{}') AS work`
|
||||
|
||||
const query = renderSql(
|
||||
select(selectCols),
|
||||
@@ -267,7 +313,8 @@ export const getProfiles: APIHandler<'get-profiles'> = async (props, auth) => {
|
||||
if (!props.skipId) props.skipId = auth.uid
|
||||
const {profiles, count} = await loadProfiles(props)
|
||||
return {status: 'success', profiles: profiles, count: count}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return {status: 'fail', profiles: [], count: 0}
|
||||
}
|
||||
}
|
||||
|
||||
63
backend/api/src/update-options.ts
Normal file
63
backend/api/src/update-options.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {APIError, APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {log} from 'shared/utils'
|
||||
import {tryCatch} from 'common/util/try-catch'
|
||||
import {OPTION_TABLES} from "common/profiles/constants";
|
||||
|
||||
export const updateOptions: APIHandler<'update-options'> = async (
|
||||
{table, names},
|
||||
auth
|
||||
) => {
|
||||
if (!OPTION_TABLES.includes(table)) throw new APIError(400, 'Invalid table')
|
||||
if (!names || !Array.isArray(names) || names.length === 0) {
|
||||
throw new APIError(400, 'No names provided')
|
||||
}
|
||||
|
||||
log('Updating profile options', {table, names})
|
||||
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
const profileIdResult = await pg.oneOrNone<{ id: number }>(
|
||||
'SELECT id FROM profiles WHERE user_id = $1',
|
||||
[auth.uid]
|
||||
)
|
||||
if (!profileIdResult) throw new APIError(404, 'Profile not found')
|
||||
const profileId = profileIdResult.id
|
||||
|
||||
const result = await tryCatch(pg.tx(async (t) => {
|
||||
const ids: number[] = []
|
||||
for (const name of names) {
|
||||
const row = await t.one<{ id: number }>(
|
||||
`INSERT INTO ${table} (name, creator_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (name) DO UPDATE
|
||||
SET name = ${table}.name
|
||||
RETURNING id`,
|
||||
[name, auth.uid]
|
||||
)
|
||||
ids.push(row.id)
|
||||
}
|
||||
|
||||
// Delete old options for this profile
|
||||
await t.none(`DELETE FROM profile_${table} WHERE profile_id = $1`, [profileId])
|
||||
|
||||
// Insert new option_ids
|
||||
if (ids.length > 0) {
|
||||
const values = ids.map((id, i) => `($1, $${i + 2})`).join(', ')
|
||||
await t.none(
|
||||
`INSERT INTO profile_${table} (profile_id, option_id) VALUES ${values}`,
|
||||
[profileId, ...ids]
|
||||
)
|
||||
}
|
||||
|
||||
return ids
|
||||
}))
|
||||
|
||||
if (result.error) {
|
||||
log('Error updating profile options', result.error)
|
||||
throw new APIError(500, 'Error updating profile options')
|
||||
}
|
||||
|
||||
return {updatedIds: result.data}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const updateProfile: APIHandler<'update-profile'> = async (
|
||||
parsedBody,
|
||||
auth
|
||||
) => {
|
||||
log('parsedBody', parsedBody)
|
||||
log('Updating profile', parsedBody)
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
const { data: existingProfile } = await tryCatch(
|
||||
|
||||
18
backend/supabase/causes.sql
Normal file
18
backend/supabase/causes.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS causes
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
creator_id text REFERENCES users (id) ON DELETE set null,
|
||||
name TEXT NOT NULL,
|
||||
CONSTRAINT causes_name_unique UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE causes
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON causes;
|
||||
CREATE POLICY "public read" ON causes
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE UNIQUE INDEX idx_causes_name_ci
|
||||
ON causes (name);
|
||||
18
backend/supabase/interests.sql
Normal file
18
backend/supabase/interests.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS interests
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
creator_id text REFERENCES users (id) ON DELETE set null,
|
||||
name TEXT NOT NULL,
|
||||
CONSTRAINT interests_name_unique UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE interests
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON interests;
|
||||
CREATE POLICY "public read" ON interests
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE UNIQUE INDEX idx_interests_name_ci
|
||||
ON interests (name);
|
||||
23
backend/supabase/profile_causes.sql
Normal file
23
backend/supabase/profile_causes.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE profile_causes
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
|
||||
option_id BIGINT NOT NULL REFERENCES causes (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE profile_causes
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON profile_causes;
|
||||
CREATE POLICY "public read" ON profile_causes
|
||||
FOR SELECT USING (true);
|
||||
|
||||
ALTER TABLE profile_causes
|
||||
ADD CONSTRAINT profile_causes_option_unique UNIQUE (profile_id, option_id);
|
||||
|
||||
CREATE INDEX idx_profile_causes_profile
|
||||
ON profile_causes (profile_id);
|
||||
|
||||
CREATE INDEX idx_profile_causes_interest
|
||||
ON profile_causes (option_id);
|
||||
23
backend/supabase/profile_interests.sql
Normal file
23
backend/supabase/profile_interests.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE profile_interests
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
|
||||
option_id BIGINT NOT NULL REFERENCES interests (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE profile_interests
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON profile_interests;
|
||||
CREATE POLICY "public read" ON profile_interests
|
||||
FOR SELECT USING (true);
|
||||
|
||||
ALTER TABLE profile_interests
|
||||
ADD CONSTRAINT profile_interests_option_unique UNIQUE (profile_id, option_id);
|
||||
|
||||
CREATE INDEX idx_profile_interests_profile
|
||||
ON profile_interests (profile_id);
|
||||
|
||||
CREATE INDEX idx_profile_interests_interest
|
||||
ON profile_interests (option_id);
|
||||
23
backend/supabase/profile_work.sql
Normal file
23
backend/supabase/profile_work.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE profile_work
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
profile_id BIGINT NOT NULL REFERENCES profiles (id) ON DELETE CASCADE,
|
||||
option_id BIGINT NOT NULL REFERENCES work (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE profile_work
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON profile_work;
|
||||
CREATE POLICY "public read" ON profile_work
|
||||
FOR SELECT USING (true);
|
||||
|
||||
ALTER TABLE profile_work
|
||||
ADD CONSTRAINT profile_work_option_unique UNIQUE (profile_id, option_id);
|
||||
|
||||
CREATE INDEX idx_profile_work_profile
|
||||
ON profile_work (profile_id);
|
||||
|
||||
CREATE INDEX idx_profile_work_interest
|
||||
ON profile_work (option_id);
|
||||
18
backend/supabase/work.sql
Normal file
18
backend/supabase/work.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS work
|
||||
(
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
creator_id text REFERENCES users (id) ON DELETE set null,
|
||||
name TEXT NOT NULL,
|
||||
CONSTRAINT work_name_unique UNIQUE (name)
|
||||
);
|
||||
|
||||
-- Row Level Security
|
||||
ALTER TABLE work
|
||||
ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "public read" ON work;
|
||||
CREATE POLICY "public read" ON work
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE UNIQUE INDEX idx_work_name_ci
|
||||
ON work (name);
|
||||
@@ -1,7 +1,7 @@
|
||||
import {arraybeSchema, baseProfilesSchema, combinedProfileSchema, contentSchema, zBoolean,} from 'common/api/zod-types'
|
||||
import {PrivateChatMessage} from 'common/chat-message'
|
||||
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants'
|
||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH, OPTION_TABLES} from 'common/profiles/constants'
|
||||
import {Profile, ProfileRow} from 'common/profiles/profile'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
import {PrivateUser, User} from 'common/user'
|
||||
@@ -460,6 +460,9 @@ export const API = (_apiTypeCheck = {
|
||||
diet: arraybeSchema.optional(),
|
||||
political_beliefs: arraybeSchema.optional(),
|
||||
mbti: arraybeSchema.optional(),
|
||||
interests: arraybeSchema.optional(),
|
||||
causes: arraybeSchema.optional(),
|
||||
work: arraybeSchema.optional(),
|
||||
relationship_status: arraybeSchema.optional(),
|
||||
languages: arraybeSchema.optional(),
|
||||
wants_kids_strength: z.coerce.number().optional(),
|
||||
@@ -486,6 +489,33 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'List profiles with filters, pagination and ordering',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'get-options': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {},
|
||||
props: z
|
||||
.object({
|
||||
table: z.enum(OPTION_TABLES),
|
||||
})
|
||||
.strict(),
|
||||
summary: 'Get profile options like interests',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'update-options': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {},
|
||||
props: z
|
||||
.object({
|
||||
table: z.enum(OPTION_TABLES),
|
||||
names: arraybeSchema.optional(),
|
||||
})
|
||||
.strict(),
|
||||
summary: 'Update profile options like interests',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'create-comment': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
|
||||
@@ -91,6 +91,9 @@ const optionalProfilesSchema = z.object({
|
||||
occupation: z.string().optional().nullable(),
|
||||
occupation_title: z.string().optional().nullable(),
|
||||
political_beliefs: z.array(z.string()).optional().nullable(),
|
||||
interests: z.array(z.string()).optional().nullable(),
|
||||
work: z.array(z.string()).optional().nullable(),
|
||||
causes: z.array(z.string()).optional().nullable(),
|
||||
relationship_status: z.array(z.string()).optional().nullable(),
|
||||
political_details: z.string().optional().nullable(),
|
||||
pref_romantic_styles: z.array(z.string()).nullable(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Profile, ProfileRow} from "common/profiles/profile";
|
||||
import {cloneDeep} from "lodash";
|
||||
import {filterDefined} from "common/util/array";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
// export type TargetArea = {
|
||||
// lat: number
|
||||
@@ -21,7 +22,10 @@ export type FilterFields = {
|
||||
shortBio: boolean | undefined
|
||||
drinks_min: number | undefined
|
||||
drinks_max: number | undefined
|
||||
} & Pick<
|
||||
} & {
|
||||
[K in OptionTableKey]: string[]
|
||||
}
|
||||
& Pick<
|
||||
ProfileRow,
|
||||
| 'wants_kids_strength'
|
||||
| 'pref_relation_styles'
|
||||
@@ -74,6 +78,9 @@ export const initialFilters: Partial<FilterFields> = {
|
||||
pref_romantic_styles: undefined,
|
||||
diet: undefined,
|
||||
political_beliefs: undefined,
|
||||
interests: undefined,
|
||||
causes: undefined,
|
||||
work: undefined,
|
||||
relationship_status: undefined,
|
||||
languages: undefined,
|
||||
religion: undefined,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { isProd } from 'common/envs/is-prod'
|
||||
import {isProd} from 'common/envs/is-prod'
|
||||
|
||||
export const compassUserId = isProd()
|
||||
? 'tRZZ6ihugZQLXPf6aPRneGpWLmz1'
|
||||
: 'RlXR2xa4EFfAzdCbSe45wkcdarh1'
|
||||
|
||||
export const MAX_COMPATIBILITY_QUESTION_LENGTH = 240
|
||||
|
||||
export const OPTION_TABLES = ['interests', 'causes', 'work'] as const
|
||||
export type OptionTableKey = typeof OPTION_TABLES[number]
|
||||
|
||||
@@ -1,10 +1,55 @@
|
||||
import { Row, run, SupabaseClient } from 'common/supabase/utils'
|
||||
import { User } from 'common/user'
|
||||
import {Row, run, SupabaseClient} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
export type ProfileRow = Row<'profiles'>
|
||||
export type Profile = ProfileRow & { user: User }
|
||||
export const getProfileRow = async (userId: string, db: SupabaseClient) => {
|
||||
// console.debug('getProfileRow', userId)
|
||||
const res = await run(db.from('profiles').select('*').eq('user_id', userId))
|
||||
return res.data[0]
|
||||
export type ProfileWithoutUser = ProfileRow & {[K in OptionTableKey]?: string[]}
|
||||
export type Profile = ProfileWithoutUser & { user: User }
|
||||
|
||||
export const getProfileRow = async (userId: string, db: SupabaseClient): Promise<ProfileWithoutUser | null> => {
|
||||
// Fetch profile
|
||||
const profileRes = await run(
|
||||
db
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
)
|
||||
const profile = profileRes.data?.[0]
|
||||
if (!profile) return null
|
||||
|
||||
// Fetch interests
|
||||
const interestsRes = await run(
|
||||
db
|
||||
.from('profile_interests')
|
||||
.select('interests(name)')
|
||||
.eq('profile_id', profile.id)
|
||||
)
|
||||
const interests = interestsRes.data?.map((row: any) => row.interests.name) || []
|
||||
|
||||
// Fetch causes
|
||||
const causesRes = await run(
|
||||
db
|
||||
.from('profile_causes')
|
||||
.select('causes(name)')
|
||||
.eq('profile_id', profile.id)
|
||||
)
|
||||
const causes = causesRes.data?.map((row: any) => row.causes.name) || []
|
||||
|
||||
// Fetch causes
|
||||
const workRes = await run(
|
||||
db
|
||||
.from('profile_work')
|
||||
.select('work(name)')
|
||||
.eq('profile_id', profile.id)
|
||||
)
|
||||
const work = workRes.data?.map((row: any) => row.work.name) || []
|
||||
|
||||
// console.debug('work', work)
|
||||
|
||||
return {
|
||||
...profile,
|
||||
interests,
|
||||
causes,
|
||||
work,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,32 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
causes: {
|
||||
Row: {
|
||||
creator_id: string | null
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'causes_creator_id_fkey'
|
||||
columns: ['creator_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
compatibility_answers: {
|
||||
Row: {
|
||||
created_time: string
|
||||
@@ -243,6 +269,32 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
interests: {
|
||||
Row: {
|
||||
creator_id: string | null
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'interests_creator_id_fkey'
|
||||
columns: ['creator_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
private_user_message_channel_members: {
|
||||
Row: {
|
||||
channel_id: number
|
||||
@@ -431,6 +483,39 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
profile_causes: {
|
||||
Row: {
|
||||
id: number
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Insert: {
|
||||
id?: never
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Update: {
|
||||
id?: never
|
||||
option_id?: number
|
||||
profile_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'profile_causes_option_id_fkey'
|
||||
columns: ['option_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'causes'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'profile_causes_profile_id_fkey'
|
||||
columns: ['profile_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'profiles'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
profile_comments: {
|
||||
Row: {
|
||||
content: Json
|
||||
@@ -485,6 +570,39 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
profile_interests: {
|
||||
Row: {
|
||||
id: number
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Insert: {
|
||||
id?: never
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Update: {
|
||||
id?: never
|
||||
option_id?: number
|
||||
profile_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'profile_interests_option_id_fkey'
|
||||
columns: ['option_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'interests'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'profile_interests_profile_id_fkey'
|
||||
columns: ['profile_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'profiles'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
profile_likes: {
|
||||
Row: {
|
||||
created_time: string
|
||||
@@ -582,6 +700,39 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
profile_work: {
|
||||
Row: {
|
||||
id: number
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Insert: {
|
||||
id?: never
|
||||
option_id: number
|
||||
profile_id: number
|
||||
}
|
||||
Update: {
|
||||
id?: never
|
||||
option_id?: number
|
||||
profile_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'profile_work_option_id_fkey'
|
||||
columns: ['option_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'work'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'profile_work_profile_id_fkey'
|
||||
columns: ['profile_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'profiles'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
profiles: {
|
||||
Row: {
|
||||
age: number | null
|
||||
@@ -1077,6 +1228,32 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
work: {
|
||||
Row: {
|
||||
creator_id: string | null
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
creator_id?: string | null
|
||||
id?: never
|
||||
name?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'work_creator_id_fkey'
|
||||
columns: ['creator_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
|
||||
31
web/components/add-option-entry.tsx
Normal file
31
web/components/add-option-entry.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {ProfileWithoutUser} from "common/profiles/profile";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
import {Col} from "web/components/layout/col";
|
||||
import clsx from "clsx";
|
||||
import {colClassName, labelClassName} from "web/pages/signup";
|
||||
import {MultiCheckbox} from "web/components/multi-checkbox";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
export function AddOptionEntry(props: {
|
||||
choices: {},
|
||||
setChoices: (choices: {}) => void
|
||||
profile: ProfileWithoutUser,
|
||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
||||
label: OptionTableKey,
|
||||
}) {
|
||||
const {profile, setProfile, label, choices, setChoices} = props
|
||||
return <Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>{capitalize(label)}</label>
|
||||
<MultiCheckbox
|
||||
choices={choices}
|
||||
selected={profile[label] ?? []}
|
||||
onChange={(selected) => setProfile(label, selected)}
|
||||
addOption={(v: string) => {
|
||||
console.log(`Adding ${label}:`, v)
|
||||
setChoices((prev: string[]) => ({...prev, [v]: v}))
|
||||
setProfile(label, [...(profile[label] ?? []), v])
|
||||
return {key: v, value: v}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import {KidsLabel, wantsKidsLabelsWithIcon} from "web/components/filters/wants-k
|
||||
import {hasKidsLabels} from "common/has-kids";
|
||||
import {HasKidsLabel} from "web/components/filters/has-kids-filter";
|
||||
import {RomanticFilter, RomanticFilterText} from "web/components/filters/romantic-filter";
|
||||
import {FaHeart} from "react-icons/fa";
|
||||
import {FaBriefcase, FaHandsHelping, FaHeart, FaStar} from "react-icons/fa";
|
||||
import {DietFilter, DietFilterText} from "web/components/filters/diet-filter";
|
||||
import {PoliticalFilter, PoliticalFilterText} from "web/components/filters/political-filter";
|
||||
import {GiFruitBowl} from "react-icons/gi";
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
RelationshipStatusFilterText
|
||||
} from "web/components/filters/relationship-status-filter";
|
||||
import {BsPersonHeart} from "react-icons/bs";
|
||||
import {InterestFilter, InterestFilterText} from "web/components/filters/interest-filter";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
export function DesktopFilters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
@@ -50,6 +52,7 @@ export function DesktopFilters(props: {
|
||||
isYourFilters: boolean
|
||||
locationFilterProps: LocationFilterProps
|
||||
includeRelationshipFilters: boolean | undefined
|
||||
choices: Record<OptionTableKey, Record<string, string[]>>
|
||||
}) {
|
||||
const {
|
||||
filters,
|
||||
@@ -60,6 +63,7 @@ export function DesktopFilters(props: {
|
||||
isYourFilters,
|
||||
locationFilterProps,
|
||||
includeRelationshipFilters,
|
||||
choices,
|
||||
} = props
|
||||
|
||||
return (
|
||||
@@ -418,6 +422,105 @@ export function DesktopFilters(props: {
|
||||
menuWidth="w-50"
|
||||
/>
|
||||
|
||||
{/* Interests */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open) => (
|
||||
<DropdownButton
|
||||
open={open}
|
||||
content={
|
||||
<Row className="items-center gap-1">
|
||||
<FaStar className="h-4 w-4"/>
|
||||
<InterestFilterText
|
||||
options={
|
||||
filters.interests as
|
||||
| string[]
|
||||
| undefined
|
||||
}
|
||||
highlightedClass={open ? 'text-primary-500' : undefined}
|
||||
label={'interest'}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
dropdownMenuContent={
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.interests}
|
||||
label={'interests'}
|
||||
/>
|
||||
}
|
||||
popoverClassName="bg-canvas-50"
|
||||
menuWidth="w-50 max-h-[400px] overflow-y-auto"
|
||||
/>
|
||||
|
||||
{/* Causes */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open) => (
|
||||
<DropdownButton
|
||||
open={open}
|
||||
content={
|
||||
<Row className="items-center gap-1">
|
||||
<FaHandsHelping className="h-4 w-4"/>
|
||||
<InterestFilterText
|
||||
options={
|
||||
filters.causes as
|
||||
| string[]
|
||||
| undefined
|
||||
}
|
||||
highlightedClass={open ? 'text-primary-500' : undefined}
|
||||
label={'cause'}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
dropdownMenuContent={
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.causes}
|
||||
label={'causes'}
|
||||
/>
|
||||
}
|
||||
popoverClassName="bg-canvas-50"
|
||||
menuWidth="w-50 max-h-[400px] overflow-y-auto"
|
||||
/>
|
||||
|
||||
{/* Work */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open) => (
|
||||
<DropdownButton
|
||||
open={open}
|
||||
content={
|
||||
<Row className="items-center gap-1">
|
||||
<FaBriefcase className="h-4 w-4"/>
|
||||
<InterestFilterText
|
||||
options={
|
||||
filters.work as
|
||||
| string[]
|
||||
| undefined
|
||||
}
|
||||
highlightedClass={open ? 'text-primary-500' : undefined}
|
||||
label={'work'}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
dropdownMenuContent={
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.work}
|
||||
label={'work'}
|
||||
/>
|
||||
}
|
||||
popoverClassName="bg-canvas-50"
|
||||
menuWidth="w-50 max-h-[400px] overflow-y-auto"
|
||||
/>
|
||||
|
||||
{/* POLITICS */}
|
||||
<CustomizeableDropdown
|
||||
buttonContent={(open) => (
|
||||
|
||||
59
web/components/filters/interest-filter.tsx
Normal file
59
web/components/filters/interest-filter.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import clsx from 'clsx'
|
||||
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
|
||||
import {MultiCheckbox} from 'web/components/multi-checkbox'
|
||||
import {FilterFields} from "common/filters";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
export function InterestFilterText(props: {
|
||||
options: string[] | undefined
|
||||
highlightedClass?: string
|
||||
label: string
|
||||
}) {
|
||||
const {options, highlightedClass, label} = props
|
||||
const length = (options ?? []).length
|
||||
|
||||
if (!options || length < 1) {
|
||||
return (
|
||||
<span className={clsx('text-semibold', highlightedClass)}>Any {label}</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (length > 2) {
|
||||
return (
|
||||
<span>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
Multiple
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={clsx('font-semibold', highlightedClass)}>
|
||||
{stringOrStringArrayToText({
|
||||
text: options,
|
||||
capitalizeFirstLetterOption: true,
|
||||
})}{' '}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function InterestFilter(props: {
|
||||
filters: Partial<FilterFields>
|
||||
updateFilter: (newState: Partial<FilterFields>) => void
|
||||
choices: Record<string, string[]>
|
||||
label: OptionTableKey
|
||||
}) {
|
||||
const {filters, updateFilter, choices, label} = props
|
||||
return (
|
||||
<MultiCheckbox
|
||||
selected={filters[label] ?? []}
|
||||
choices={choices as any}
|
||||
onChange={(c) => {
|
||||
updateFilter({[label]: c})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
RelationshipStatusFilterText
|
||||
} from "web/components/filters/relationship-status-filter";
|
||||
import {MbtiFilter, MbtiFilterText} from "web/components/filters/mbti-filter";
|
||||
import {InterestFilter, InterestFilterText} from "web/components/filters/interest-filter";
|
||||
import {OptionTableKey} from "common/profiles/constants";
|
||||
|
||||
function MobileFilters(props: {
|
||||
filters: Partial<FilterFields>
|
||||
@@ -41,6 +43,7 @@ function MobileFilters(props: {
|
||||
isYourFilters: boolean
|
||||
locationFilterProps: LocationFilterProps
|
||||
includeRelationshipFilters: boolean | undefined
|
||||
choices: Record<OptionTableKey, Record<string, string[]>>
|
||||
}) {
|
||||
const {
|
||||
filters,
|
||||
@@ -51,6 +54,7 @@ function MobileFilters(props: {
|
||||
isYourFilters,
|
||||
locationFilterProps,
|
||||
includeRelationshipFilters,
|
||||
choices,
|
||||
} = props
|
||||
|
||||
const [openFilter, setOpenFilter] = useState<string | undefined>(undefined)
|
||||
@@ -364,6 +368,84 @@ function MobileFilters(props: {
|
||||
<LanguageFilter filters={filters} updateFilter={updateFilter}/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* INTERESTS */}
|
||||
<MobileFilterSection
|
||||
title="Interests"
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.interests || undefined)}
|
||||
selection={
|
||||
<InterestFilterText
|
||||
options={filters.interests as string[]}
|
||||
highlightedClass={
|
||||
hasAny(filters.interests || undefined)
|
||||
? 'text-primary-600'
|
||||
: 'text-ink-900'
|
||||
}
|
||||
label={'interests'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.interests}
|
||||
label={'interests'}
|
||||
/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* CAUSES */}
|
||||
<MobileFilterSection
|
||||
title="Causes"
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.causes || undefined)}
|
||||
selection={
|
||||
<InterestFilterText
|
||||
options={filters.causes as string[]}
|
||||
highlightedClass={
|
||||
hasAny(filters.causes || undefined)
|
||||
? 'text-primary-600'
|
||||
: 'text-ink-900'
|
||||
}
|
||||
label={'causes'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.causes}
|
||||
label={'causes'}
|
||||
/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* WORK */}
|
||||
<MobileFilterSection
|
||||
title="Work"
|
||||
openFilter={openFilter}
|
||||
setOpenFilter={setOpenFilter}
|
||||
isActive={hasAny(filters.work || undefined)}
|
||||
selection={
|
||||
<InterestFilterText
|
||||
options={filters.work as string[]}
|
||||
highlightedClass={
|
||||
hasAny(filters.work || undefined)
|
||||
? 'text-primary-600'
|
||||
: 'text-ink-900'
|
||||
}
|
||||
label={'work'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InterestFilter
|
||||
filters={filters}
|
||||
updateFilter={updateFilter}
|
||||
choices={choices.work}
|
||||
label={'work'}
|
||||
/>
|
||||
</MobileFilterSection>
|
||||
|
||||
{/* POLITICS */}
|
||||
<MobileFilterSection
|
||||
title="Politics"
|
||||
|
||||
@@ -17,6 +17,7 @@ import {useUser} from "web/hooks/use-user";
|
||||
import toast from "react-hot-toast";
|
||||
import {FilterFields} from "common/filters";
|
||||
import {DisplayUser} from "common/api/user-types";
|
||||
import {useChoices} from "web/hooks/use-choices";
|
||||
|
||||
function isOrderBy(input: string): input is FilterFields['orderBy'] {
|
||||
return ['last_online_time', 'created_time', 'compatibility_score'].includes(
|
||||
@@ -137,6 +138,14 @@ export const Search = (props: {
|
||||
const [openStarBookmarks, setOpenStarBookmarks] = useState(false);
|
||||
const user = useUser()
|
||||
const youSeekingRelationship = youProfile?.pref_relation_styles?.includes('relationship')
|
||||
const {choices: interestChoices} = useChoices('interests')
|
||||
const {choices: causeChoices} = useChoices('causes')
|
||||
const {choices: workChoices} = useChoices('work')
|
||||
const choices = {
|
||||
interests: interestChoices,
|
||||
causes: causeChoices,
|
||||
work: workChoices,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isHolding) return;
|
||||
@@ -221,6 +230,7 @@ export const Search = (props: {
|
||||
isYourFilters={isYourFilters}
|
||||
locationFilterProps={locationFilterProps}
|
||||
includeRelationshipFilters={youSeekingRelationship}
|
||||
choices={choices}
|
||||
/>
|
||||
</Row>
|
||||
<RightModal
|
||||
@@ -237,6 +247,7 @@ export const Search = (props: {
|
||||
isYourFilters={isYourFilters}
|
||||
locationFilterProps={locationFilterProps}
|
||||
includeRelationshipFilters={youSeekingRelationship}
|
||||
choices={choices}
|
||||
/>
|
||||
</RightModal>
|
||||
<Row className="items-center justify-between w-full flex-wrap gap-2">
|
||||
|
||||
@@ -74,6 +74,9 @@ export const useFilters = (you: Profile | undefined) => {
|
||||
pref_romantic_styles: you?.pref_romantic_styles?.length ? you.pref_romantic_styles : undefined,
|
||||
diet: you?.diet?.length ? you.diet : undefined,
|
||||
political_beliefs: you?.political_beliefs?.length ? you.political_beliefs : undefined,
|
||||
interests: you?.interests?.length ? you.interests : undefined,
|
||||
work: you?.work?.length ? you.work : undefined,
|
||||
causes: you?.causes?.length ? you.causes : undefined,
|
||||
mbti: you?.mbti ? [you.mbti] : undefined,
|
||||
relationship_status: you?.relationship_status?.length ? you.relationship_status : undefined,
|
||||
languages: you?.languages?.length ? you.languages : undefined,
|
||||
@@ -99,6 +102,9 @@ export const useFilters = (you: Profile | undefined) => {
|
||||
&& isEqual(new Set(filters.pref_relation_styles), new Set(you.pref_relation_styles))
|
||||
&& isEqual(new Set(filters.diet), new Set(you.diet))
|
||||
&& isEqual(new Set(filters.political_beliefs), new Set(you.political_beliefs))
|
||||
&& isEqual(new Set(filters.interests), new Set(you.interests))
|
||||
&& isEqual(new Set(filters.causes), new Set(you.causes))
|
||||
&& isEqual(new Set(filters.work), new Set(you.work))
|
||||
&& isEqual(new Set(filters.relationship_status), new Set(you.relationship_status))
|
||||
&& isEqual(new Set(filters.languages), new Set(you.languages))
|
||||
&& isEqual(new Set(filters.religion), new Set(you.religion))
|
||||
|
||||
@@ -1,30 +1,134 @@
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Checkbox } from 'web/components/widgets/checkbox'
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Input } from 'web/components/widgets/input'
|
||||
import { Button } from 'web/components/buttons/button'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export const MultiCheckbox = (props: {
|
||||
// Map of label -> value
|
||||
choices: { [key: string]: string }
|
||||
// Selected values (should match the "value" side of choices)
|
||||
selected: string[]
|
||||
onChange: (selected: string[]) => void
|
||||
className?: string
|
||||
// If provided, enables adding a new option and should persist it (e.g. to DB)
|
||||
// Return value can be:
|
||||
// - string: the stored value for the new option; label will be the input text
|
||||
// - { key, value }: explicit label (key) and stored value
|
||||
// - null/undefined to indicate failure/cancellation
|
||||
addOption?: (label: string) => string | { key: string; value: string } | null | undefined
|
||||
addPlaceholder?: string
|
||||
}) => {
|
||||
const { choices, selected, onChange, className } = props
|
||||
const { choices, selected, onChange, className, addOption, addPlaceholder } = props
|
||||
|
||||
// Keep a local merged copy to allow optimistic adds while remaining in sync with props
|
||||
const [localChoices, setLocalChoices] = useState<{ [key: string]: string }>(choices)
|
||||
useEffect(() => {
|
||||
setLocalChoices((prev) => {
|
||||
// If incoming choices changed, merge them with any locally added that still don't collide
|
||||
// Props should be source of truth on conflicts
|
||||
return { ...prev, ...choices }
|
||||
})
|
||||
}, [choices])
|
||||
|
||||
const entries = useMemo(() => Object.entries(localChoices), [localChoices])
|
||||
|
||||
// Add-new option state
|
||||
const [newLabel, setNewLabel] = useState('')
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Filter visible options while typing a new option (case-insensitive label match)
|
||||
const filteredEntries = useMemo(() => {
|
||||
if (!addOption) return entries
|
||||
const q = newLabel.trim().toLowerCase()
|
||||
if (!q) return entries
|
||||
return entries.filter(([key]) => key.toLowerCase().includes(q))
|
||||
}, [addOption, entries, newLabel])
|
||||
|
||||
const submitAdd = async () => {
|
||||
if (!addOption) return
|
||||
const label = newLabel.trim()
|
||||
setError(null)
|
||||
if (!label) {
|
||||
setError('Please enter a value.')
|
||||
return
|
||||
}
|
||||
// prevent duplicate by label or by value already selected
|
||||
const lowerCaseChoices = Object.keys(localChoices).map((k: string) => k.toLowerCase())
|
||||
if (lowerCaseChoices.includes(label.toLowerCase())) {
|
||||
setError('That option already exists.')
|
||||
// const key = Object.keys(lowerCaseChoices).find((k) => k.toLowerCase() === label.toLowerCase())
|
||||
// if (!key) return
|
||||
// setProfile('interests', [...(profile['interests'] ?? []), key])
|
||||
return
|
||||
}
|
||||
setAdding(true)
|
||||
try {
|
||||
const result = addOption(label)
|
||||
if (!result) {
|
||||
setError('Could not add option.')
|
||||
setAdding(false)
|
||||
return
|
||||
}
|
||||
const { key, value } = typeof result === 'string' ? { key: label, value: result } : result
|
||||
setLocalChoices((prev) => ({ ...prev, [key]: value }))
|
||||
// auto-select newly added option if not already selected
|
||||
if (!selected.includes(value)) onChange([...selected, value])
|
||||
setNewLabel('')
|
||||
} catch (e) {
|
||||
setError('Failed to add option.')
|
||||
} finally {
|
||||
setAdding(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className={clsx('flex-wrap', className)}>
|
||||
{Object.entries(choices).map(([key, value]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={key}
|
||||
checked={selected.includes(value)}
|
||||
toggle={(checked: boolean) => {
|
||||
if (checked) {
|
||||
onChange([...selected, value])
|
||||
} else {
|
||||
onChange(selected.filter((s) => s !== value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
<div className={clsx('space-y-2', className)}>
|
||||
{addOption && (
|
||||
<Row className="items-center gap-2">
|
||||
<Input
|
||||
value={newLabel}
|
||||
placeholder={addPlaceholder ?? 'Search or add'}
|
||||
onChange={(e) => {
|
||||
setNewLabel(e.target.value)
|
||||
setError(null)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
submitAdd()
|
||||
}
|
||||
}}
|
||||
className="h-10"
|
||||
/>
|
||||
<Button size="sm" onClick={submitAdd} loading={adding} disabled={adding}>
|
||||
Add
|
||||
</Button>
|
||||
{error && <span className="text-sm text-error">{error}</span>}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row className={clsx('flex-wrap')}>
|
||||
{filteredEntries.map(([key, value]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={key}
|
||||
checked={selected.includes(value)}
|
||||
toggle={(checked: boolean) => {
|
||||
if (checked) {
|
||||
onChange([...selected, value])
|
||||
} else {
|
||||
onChange(selected.filter((s) => s !== value))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
{addOption && newLabel.trim() && filteredEntries.length === 0 && (
|
||||
<div className="px-2 text-sm text-ink-500">No matching options, feel free to add it.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Fragment, useRef, useState} from 'react'
|
||||
import {Fragment, useEffect, useRef, useState} from 'react'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import clsx from 'clsx'
|
||||
@@ -9,13 +9,12 @@ import {ChoicesToggleGroup} from 'web/components/widgets/choices-toggle-group'
|
||||
import {Button, IconButton} from 'web/components/buttons/button'
|
||||
import {colClassName, labelClassName} from 'web/pages/signup'
|
||||
import {useRouter} from 'next/router'
|
||||
import {updateProfile, updateUser} from 'web/lib/api'
|
||||
import {Column} from 'common/supabase/utils'
|
||||
import {api, updateProfile, updateUser} from 'web/lib/api'
|
||||
import {User} from 'common/user'
|
||||
import {track} from 'web/lib/service/analytics'
|
||||
import {Carousel} from 'web/components/widgets/carousel'
|
||||
import {tryCatch} from 'common/util/try-catch'
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {removeUndefinedProps} from 'common/util/object'
|
||||
import {isEqual, range} from 'lodash'
|
||||
import {PlatformSelect} from 'web/components/widgets/platform-select'
|
||||
@@ -32,7 +31,7 @@ import {
|
||||
EDUCATION_CHOICES,
|
||||
LANGUAGE_CHOICES,
|
||||
MBTI_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
POLITICAL_CHOICES,
|
||||
RACE_CHOICES,
|
||||
RELATIONSHIP_CHOICES,
|
||||
RELATIONSHIP_STATUS_CHOICES,
|
||||
@@ -40,10 +39,14 @@ import {
|
||||
ROMANTIC_CHOICES
|
||||
} from "web/components/filters/choices";
|
||||
import toast from "react-hot-toast";
|
||||
import {db} from "web/lib/supabase/db";
|
||||
import {fetchChoices} from "web/hooks/use-choices";
|
||||
import {AddOptionEntry} from "web/components/add-option-entry";
|
||||
|
||||
|
||||
export const OptionalProfileUserForm = (props: {
|
||||
profile: ProfileRow
|
||||
setProfile: <K extends Column<'profiles'>>(key: K, value: ProfileRow[K]) => void
|
||||
profile: ProfileWithoutUser
|
||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void
|
||||
user: User
|
||||
buttonLabel?: string
|
||||
fromSignup?: boolean
|
||||
@@ -71,15 +74,44 @@ export const OptionalProfileUserForm = (props: {
|
||||
|
||||
const [newLinkPlatform, setNewLinkPlatform] = useState('')
|
||||
const [newLinkValue, setNewLinkValue] = useState('')
|
||||
const [interestChoices, setInterestChoices] = useState({})
|
||||
const [causeChoices, setCauseChoices] = useState({})
|
||||
const [workChoices, setWorkChoices] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchChoices('interests').then(setInterestChoices)
|
||||
fetchChoices('causes').then(setCauseChoices)
|
||||
fetchChoices('work').then(setWorkChoices)
|
||||
}, [db])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true)
|
||||
const {bio: _bio, bio_text: _bio_text, bio_tsv: _bio_tsv, bio_length: _bio_length, ...otherProfileProps} = profile
|
||||
const {
|
||||
bio: _bio,
|
||||
bio_text: _bio_text,
|
||||
bio_tsv: _bio_tsv,
|
||||
bio_length: _bio_length,
|
||||
interests,
|
||||
causes,
|
||||
work,
|
||||
...otherProfileProps
|
||||
} = profile
|
||||
console.debug('otherProfileProps', removeUndefinedProps(otherProfileProps))
|
||||
const {error} = await tryCatch(
|
||||
updateProfile(removeUndefinedProps(otherProfileProps) as any)
|
||||
)
|
||||
if (error) {
|
||||
const promises: Promise<any>[] = [
|
||||
tryCatch(updateProfile(removeUndefinedProps(otherProfileProps) as any))
|
||||
]
|
||||
if (interests?.length) {
|
||||
promises.push(api('update-options', {table: 'interests', names: interests}))
|
||||
}
|
||||
if (causes?.length) {
|
||||
promises.push(api('update-options', {table: 'causes', names: causes}))
|
||||
}
|
||||
if (work?.length) {
|
||||
promises.push(api('update-options', {table: 'work', names: work}))
|
||||
}
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error(
|
||||
`We ran into an issue saving your profile. Please try again or contact us if the issue persists.`
|
||||
@@ -94,7 +126,6 @@ export const OptionalProfileUserForm = (props: {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit && (await onSubmit())
|
||||
setIsSubmitting(false)
|
||||
track('submit optional profile')
|
||||
@@ -441,6 +472,22 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<AddOptionEntry
|
||||
choices={interestChoices}
|
||||
setChoices={setInterestChoices}
|
||||
profile={profile}
|
||||
setProfile={setProfile}
|
||||
label={'interests'}
|
||||
/>
|
||||
|
||||
<AddOptionEntry
|
||||
choices={causeChoices}
|
||||
setChoices={setCauseChoices}
|
||||
profile={profile}
|
||||
setProfile={setProfile}
|
||||
label={'causes'}
|
||||
/>
|
||||
|
||||
<Col className={clsx(colClassName, 'max-w-[550px]')}>
|
||||
<label className={clsx(labelClassName)}>MBTI Personality Type</label>
|
||||
<ChoicesToggleGroup
|
||||
@@ -460,6 +507,59 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<AddOptionEntry
|
||||
choices={workChoices}
|
||||
setChoices={setWorkChoices}
|
||||
profile={profile}
|
||||
setProfile={setProfile}
|
||||
label={'work'}
|
||||
/>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Company</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('company', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['company'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Job title {profile['company'] ? 'at ' + profile['company'] : ''}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('occupation_title', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['occupation_title'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Highest completed education level
|
||||
</label>
|
||||
<Carousel className="max-w-full">
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['education_level'] ?? ''}
|
||||
choicesMap={EDUCATION_CHOICES}
|
||||
setChoice={(c) => setProfile('education_level', c)}
|
||||
/>
|
||||
</Carousel>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>University</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('university', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['university'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Do you smoke?</label>
|
||||
<ChoicesToggleGroup
|
||||
@@ -572,49 +672,6 @@ export const OptionalProfileUserForm = (props: {
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Highest completed education level
|
||||
</label>
|
||||
<Carousel className="max-w-full">
|
||||
<ChoicesToggleGroup
|
||||
currentChoice={profile['education_level'] ?? ''}
|
||||
choicesMap={EDUCATION_CHOICES}
|
||||
setChoice={(c) => setProfile('education_level', c)}
|
||||
/>
|
||||
</Carousel>
|
||||
</Col>
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>University</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('university', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['university'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>Company</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('company', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['company'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col className={clsx(colClassName)}>
|
||||
<label className={clsx(labelClassName)}>
|
||||
Job title {profile['company'] ? 'at ' + profile['company'] : ''}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => setProfile('occupation_title', e.target.value)}
|
||||
className={'w-52'}
|
||||
value={profile['occupation_title'] ?? undefined}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/*<Col className={clsx(colClassName)}>*/}
|
||||
{/* <label className={clsx(labelClassName)}>Looking for a relationship?</label>*/}
|
||||
{/* <ChoicesToggleGroup*/}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {UserActivity} from "common/user";
|
||||
import {ClockIcon} from "@heroicons/react/solid";
|
||||
import {MAX_INT, MIN_INT} from "common/constants";
|
||||
import {GiFruitBowl} from "react-icons/gi";
|
||||
import {FaBriefcase, FaHandsHelping, FaStar} from "react-icons/fa";
|
||||
|
||||
export function AboutRow(props: {
|
||||
icon: ReactNode
|
||||
@@ -85,8 +86,8 @@ export default function ProfileAbout(props: {
|
||||
<Education profile={profile}/>
|
||||
<Occupation profile={profile}/>
|
||||
<AboutRow
|
||||
icon={<MdLanguage className="h-5 w-5"/>}
|
||||
text={profile.languages?.map(v => INVERTED_LANGUAGE_CHOICES[v])}
|
||||
icon={<FaBriefcase className="h-5 w-5"/>}
|
||||
text={profile.work}
|
||||
/>
|
||||
<AboutRow
|
||||
icon={<RiScales3Line className="h-5 w-5"/>}
|
||||
@@ -98,6 +99,14 @@ export default function ProfileAbout(props: {
|
||||
text={profile.religion?.map(belief => INVERTED_RELIGION_CHOICES[belief])}
|
||||
suffix={profile.religious_beliefs}
|
||||
/>
|
||||
<AboutRow
|
||||
icon={<FaStar className="h-5 w-5"/>}
|
||||
text={profile.interests}
|
||||
/>
|
||||
<AboutRow
|
||||
icon={<FaHandsHelping className="h-5 w-5"/>}
|
||||
text={profile.causes}
|
||||
/>
|
||||
<AboutRow
|
||||
icon={<BsPersonVcard className="h-5 w-5"/>}
|
||||
text={profile.mbti ? INVERTED_MBTI_CHOICES[profile.mbti] : null}
|
||||
@@ -114,6 +123,10 @@ export default function ProfileAbout(props: {
|
||||
icon={<GiFruitBowl className="h-5 w-5"/>}
|
||||
text={profile.diet?.map(e => INVERTED_DIET_CHOICES[e])}
|
||||
/>
|
||||
<AboutRow
|
||||
icon={<MdLanguage className="h-5 w-5"/>}
|
||||
text={profile.languages?.map(v => INVERTED_LANGUAGE_CHOICES[v])}
|
||||
/>
|
||||
<HasKids profile={profile}/>
|
||||
<WantsKids profile={profile}/>
|
||||
{!isCurrentUser && <LastOnline lastOnlineTime={userActivity?.last_online_time}/>}
|
||||
|
||||
@@ -9,8 +9,7 @@ import {labelClassName} from 'web/pages/signup'
|
||||
import {User} from 'common/user'
|
||||
import {useEditableUserInfo} from 'web/hooks/use-editable-user-info'
|
||||
import {LoadingIndicator} from 'web/components/widgets/loading-indicator'
|
||||
import {Column} from 'common/supabase/utils'
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {ProfileRow, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {SignupBio} from "web/components/bio/editable-bio";
|
||||
import {Editor} from "@tiptap/core";
|
||||
|
||||
@@ -42,7 +41,7 @@ export const RequiredProfileUserForm = (props: {
|
||||
setEditUsername?: (name: string) => unknown
|
||||
setEditDisplayName?: (name: string) => unknown
|
||||
profile: ProfileRow
|
||||
setProfile: <K extends Column<'profiles'>>(key: K, value: ProfileRow[K] | undefined) => void
|
||||
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K] | undefined) => void
|
||||
isSubmitting: boolean
|
||||
onSubmit?: () => void
|
||||
profileCreatedAlready?: boolean
|
||||
|
||||
32
web/hooks/use-choices.ts
Normal file
32
web/hooks/use-choices.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {useEffect} from 'react'
|
||||
import {run} from 'common/supabase/utils'
|
||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
|
||||
export async function fetchChoices(label: string) {
|
||||
const {data} = await run(db.from(label).select('name').order('name'))
|
||||
console.log('Fetched choices:', data)
|
||||
const results = Object.fromEntries(data.map((row: { name: string }) => [row.name, row.name]))
|
||||
return results;
|
||||
}
|
||||
|
||||
export const useChoices = (label: string) => {
|
||||
const [choices, setChoices] = usePersistentInMemoryState({}, `${label}-choices`)
|
||||
|
||||
const refreshChoices = async () => {
|
||||
try {
|
||||
const results = await fetchChoices(label)
|
||||
setChoices(results)
|
||||
} catch (err) {
|
||||
console.error('Error fetching choices:', err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Fetching choices in use effect...')
|
||||
refreshChoices()
|
||||
}, [])
|
||||
|
||||
return {choices, refreshChoices}
|
||||
}
|
||||
@@ -3,10 +3,9 @@ import {useEffect} from 'react'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||
import {User} from 'common/user'
|
||||
import {getProfileRow, Profile, ProfileRow} from 'common/profiles/profile'
|
||||
import {getProfileRow, Profile, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
|
||||
import {logger} from "common/logging";
|
||||
|
||||
export const useProfile = () => {
|
||||
const user = useUser()
|
||||
@@ -63,7 +62,7 @@ export const useProfileByUser = (user: User | undefined) => {
|
||||
|
||||
export const useProfileByUserId = (userId: string | undefined) => {
|
||||
const [profile, setProfile] = usePersistentInMemoryState<
|
||||
ProfileRow | undefined | null
|
||||
ProfileWithoutUser | undefined | null
|
||||
>(undefined, `profile-${userId}`)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Profile, ProfileRow} from 'common/profiles/profile'
|
||||
import {Column} from 'common/supabase/utils'
|
||||
import {Profile, ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {User} from 'common/user'
|
||||
import {OptionalProfileUserForm} from 'web/components/optional-profile-form'
|
||||
import {RequiredProfileUserForm} from 'web/components/required-profile-form'
|
||||
@@ -33,7 +32,7 @@ function ProfilePageInner(props: { user: User; profile: Profile }) {
|
||||
user,
|
||||
})
|
||||
|
||||
const setProfileState = <K extends Column<'profiles'>>(key: K, value: ProfileRow[K] | undefined) => {
|
||||
const setProfileState = <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K] | undefined) => {
|
||||
setProfile((prevState) => ({...prevState, [key]: value}))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {track} from 'web/lib/service/analytics'
|
||||
import {safeLocalStorage} from 'web/lib/util/local'
|
||||
import {removeNullOrUndefinedProps} from 'common/util/object'
|
||||
import {useProfileByUserId} from 'web/hooks/use-profile'
|
||||
import {ProfileRow} from 'common/profiles/profile'
|
||||
import {ProfileWithoutUser} from 'common/profiles/profile'
|
||||
import {PageBase} from "web/components/page-base";
|
||||
import {SEO} from "web/components/SEO";
|
||||
|
||||
@@ -54,10 +54,10 @@ export default function SignupPage() {
|
||||
}, [user, holdLoading])
|
||||
|
||||
// Omit the id, created_time?
|
||||
const [profileForm, setProfileForm] = useState<ProfileRow>({
|
||||
const [profileForm, setProfileForm] = useState<ProfileWithoutUser>({
|
||||
...initialRequiredState,
|
||||
} as any)
|
||||
const setProfileState = (key: keyof ProfileRow, value: any) => {
|
||||
const setProfileState = (key: keyof ProfileWithoutUser, value: any) => {
|
||||
setProfileForm((prevState) => ({...prevState, [key]: value}))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user