feat(recommendations): collect demographics and advance age cohorts
ci / ts / ts (packages + web) (push) Successful in 3m19s
ci / db / db (push) Successful in 53s
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 51s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m3s
ci / recommender / recommender (push) Failing after 16s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s
ci / ts / ts (packages + web) (push) Successful in 3m19s
ci / db / db (push) Successful in 53s
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 51s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m3s
ci / recommender / recommender (push) Failing after 16s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s
This commit is contained in:
1 parent
e158558da3
commit
fff91c446b
24 files changed
+778
-324
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { components } from '@pipu/api-client';
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { AGE_GROUPS, GENDERS, unwrap } from '@pipu/core';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,17 +20,19 @@ const MIN_ARTISTS = 3;
|
||||
* Онбординг вкуса (docs/recommendations.md#7): исполнители и жанры библиотеки — начальный вкус «Моей волны»,
|
||||
* пока своей истории почти нет. Пропуск тоже запоминается: второй раз не спросим.
|
||||
*/
|
||||
export function Onboarding({ options, onDone }: { options: Options; onDone: () => void }) {
|
||||
export function Onboarding({ options, onDone, editing = false }: { options: Options; onDone: () => void; editing?: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const session = useSession();
|
||||
const errorText = useErrorText();
|
||||
const [artists, setArtists] = useState<Set<string>>(new Set());
|
||||
const [genres, setGenres] = useState<Set<string>>(new Set());
|
||||
const [artists, setArtists] = useState<Set<string>>(new Set(options.choice?.artist_ids));
|
||||
const [genres, setGenres] = useState<Set<string>>(new Set(options.choice?.genres));
|
||||
const [gender, setGender] = useState(options.choice?.gender ?? null);
|
||||
const [ageGroup, setAgeGroup] = useState(options.choice?.age_group ?? null);
|
||||
const save = useMutation({
|
||||
mutationFn: async (skip: boolean) =>
|
||||
unwrap(
|
||||
await session.client.PUT('/api/v1/me/onboarding', {
|
||||
body: skip ? { artist_ids: [], genres: [] } : { artist_ids: [...artists], genres: [...genres] },
|
||||
body: skip ? { artist_ids: [], genres: [], gender: null, age_group: null } : { artist_ids: [...artists], genres: [...genres], gender, age_group: ageGroup },
|
||||
}),
|
||||
),
|
||||
onSuccess: onDone,
|
||||
@@ -43,7 +45,7 @@ export function Onboarding({ options, onDone }: { options: Options; onDone: () =
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
const enough = artists.size >= MIN_ARTISTS || (artists.size > 0 && genres.size > 0);
|
||||
const enough = editing || artists.size >= MIN_ARTISTS || (artists.size > 0 && genres.size > 0) || !!gender || !!ageGroup || options.artists.length === 0;
|
||||
|
||||
return (
|
||||
<div className={s.page}>
|
||||
@@ -52,6 +54,21 @@ export function Onboarding({ options, onDone }: { options: Options; onDone: () =
|
||||
<p>{t('onboarding.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<section aria-label={t('onboarding.gender')}>
|
||||
<h2 className={s.label}>{t('onboarding.gender')}</h2>
|
||||
<div className={s.genres}>
|
||||
<button type="button" className={s.genre} aria-pressed={!gender} onClick={() => setGender(null)}>{t('onboarding.unspecified')}</button>
|
||||
{GENDERS.map((g) => <button type="button" key={g} className={s.genre} aria-pressed={gender === g} onClick={() => setGender(g)}>{t(`onboarding.genders.${g}`)}</button>)}
|
||||
</div>
|
||||
</section>
|
||||
<section aria-label={t('onboarding.age_group')}>
|
||||
<h2 className={s.label}>{t('onboarding.age_group')}</h2>
|
||||
<p>{t('onboarding.age_hint')}</p>
|
||||
<div className={s.genres}>
|
||||
<button type="button" className={s.genre} aria-pressed={!ageGroup} onClick={() => setAgeGroup(null)}>{t('onboarding.unspecified')}</button>
|
||||
{AGE_GROUPS.map((g) => <button type="button" key={g} className={s.genre} aria-pressed={ageGroup === g} onClick={() => setAgeGroup(g)}>{t(`onboarding.ages.${g}`)}</button>)}
|
||||
</div>
|
||||
</section>
|
||||
<section aria-label={t('onboarding.artists')}>
|
||||
<h2 className={s.label}>{t('onboarding.artists')}</h2>
|
||||
<div className={s.artists}>
|
||||
@@ -101,11 +118,11 @@ export function Onboarding({ options, onDone }: { options: Options; onDone: () =
|
||||
|
||||
<FormError>{save.error ? errorText(save.error) : null}</FormError>
|
||||
<div className={s.actions}>
|
||||
<Button variant="ghost" busy={save.isPending && save.variables} onClick={() => save.mutate(true)}>
|
||||
{t('onboarding.skip')}
|
||||
<Button variant="ghost" disabled={save.isPending} busy={save.isPending && save.variables} onClick={() => editing ? onDone() : save.mutate(true)}>
|
||||
{t(editing ? 'form.cancel' : 'onboarding.skip')}
|
||||
</Button>
|
||||
<Button variant="primary" busy={save.isPending && !save.variables} disabled={!enough} onClick={() => save.mutate(false)}>
|
||||
{enough ? t('onboarding.done') : t('onboarding.pick_more', { count: MIN_ARTISTS - artists.size })}
|
||||
<Button variant="primary" busy={save.isPending && !save.variables} disabled={!enough || save.isPending} onClick={() => save.mutate(false)}>
|
||||
{enough ? t(editing ? 'settings.save' : 'onboarding.done') : t('onboarding.pick_more', { count: MIN_ARTISTS - artists.size })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user