Rework portal onboarding into role funnels and journeys#837
Conversation
Builders, Validators, and Community each get a role landing that adapts to
three states: signed-out, signed-in without the role, and earned. Dedicated
journey routes guide users step by step to unlock their role, and journeys
now grant the ROLE itself rather than points: farmable point grants are
removed, and points come only from verifiable tasks (starring the boilerplate
repo, posting on X). Journey progress is durable via zero-point marker
contributions that also drive a "started" state.
The Builder journey is connect GitHub plus star the boilerplate repo. The
Community journey is five verified steps (link X, link Discord, follow on X,
join Discord, and a verified X post) and grants the Creator role on
completion, with the post checked through Sorsa against the linked handle.
The Validator journey stays a single waitlist form and keeps the graduation
reward. Linking GitHub now records a contribution like X and Discord.
Community membership is only created by completing the journey: the previous
automatic creation from community contributions, POAP claims, and Discord XP
is removed. Sidebar subsections stay locked until the relevant journey is
complete, the profile shows an in-progress role with an owner-only grey
badge, and the What's New dialog is shown only to returning users.
## Implementation Notes
- frontend/src/components/funnel/: role funnel dispatcher, per-role landings,
and shared journey building blocks
- frontend/src/routes/{BuilderJourney,CommunityJourney}.svelte, ValidatorWaitlist.svelte:
dedicated journey views
- frontend/src/lib/roleState.js: funnel state machine (none/started/earned),
role/category mapping, journey paths
- frontend/src/{App,components/Sidebar,components/profile/*,routes/Profile}.svelte:
routing, locked subsections, in-progress role card and header badge
- frontend/src/components/WhatsNewDialog.svelte: gate to returning users only
- backend/users/views.py: start/complete journey endpoints, link_github_account,
community journey status and X-post verification
- backend/creators/community_journey.py, models.py, views.py: five-step status,
CommunityPostProof, journey-gated membership
- backend/{leaderboard/models,poaps/signals,community_xp/services}.py: remove
automatic Creator creation
- backend/social_tasks/sorsa_client.py: tweet lookup for X-post verification
- backend/.../migrations: community-link-github contribution type, builder star
task, CommunityPostProof
📝 WalkthroughWalkthroughBuilder and community onboarding now use role-specific funnel states, journey endpoints, a community post proof model with tweet verification, and updated frontend routing/profile surfaces. Several sync and import paths no longer create Creator profiles automatically. ChangesOnboarding funnels and role gating
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Clean up the over-engineering surfaced by the funnel review with no
behavior change. Two role landing components dropped a duplicate CTA
label that mirrored the primary one, and the validator landing's
repeated waitlist badge is now a single snippet. On the backend, the
three social-link award endpoints and the two journey-start endpoints
now delegate to shared helpers; every public endpoint and response
stays identical.
## Claude Implementation Notes
- backend/users/views.py: Add `_award_social_link_points` (link_x/discord/github become thin wrappers) and `_mark_journey_started` (start_builder/start_role become thin wrappers). Endpoints, messages and points unchanged. start_builder now also ensures a 0-point `builder-welcome` multiplier exists, matching the generic path (harmless on a 0-point type).
- frontend/src/components/funnel/CommunityLanding.svelte: Remove redundant `finalLabel`, reuse `primaryLabel`.
- frontend/src/components/funnel/ValidatorLanding.svelte: Remove redundant `finalLabel`; extract the duplicated "on the waitlist" badge markup into a `{#snippet}` rendered in both spots.
There was a problem hiding this comment.
Actionable comments posted: 48
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/contributions/migrations/0073_create_community_link_github.py`:
- Around line 21-28: The migration for ContributionType creation currently uses
Category.objects.filter(...).first(), which can silently set the category to
none if the builder category is missing. Update the migration logic around
builder_category and ContributionType.objects.update_or_create to fail closed by
explicitly checking that the builder Category exists before seeding
community-link-github, and raise an error if it does not. Keep the fix localized
to the migration so the Link GitHub Account contribution type is only created
when the builder category is present.
In `@backend/creators/community_journey.py`:
- Around line 94-127: The x_post step in step_states and journey_status is
currently marked done whenever community_post_proof exists, which can leave
Creator completion stale after an X relink. Update the x_post check to validate
that the stored proof still matches the currently linked X account before
returning done, using the current linked handle together with the proof
URL/verified author. Keep the completion logic in journey_status consistent so
the legacy join route in views does not treat an outdated proof as valid.
- Around line 69-76: The mention check in post_matches is too permissive because
it uses a substring match against genlayer_handle(), which can accept longer
handles like `@genlayerlabs`. Update the step 5 validation in post_matches to
require an exact `@handle` mention for the intended account, using a
boundary-aware match or tokenized mention parsing so only the precise handle
from genlayer_handle() passes while near matches fail.
In `@backend/creators/migrations/0002_backfill_community_members.py`:
- Around line 5-7: The historical backfill in 0002_backfill_community_members
must remain unchanged for databases that already ran it, so don’t turn it into a
no-op or alter its past behavior. Instead, add a new follow-up migration or
audited cleanup path alongside 0002_backfill_community_members to reconcile
previously auto-created Creator rows and align existing environments with the
new journey-completion-only rule. Keep the fix scoped so the migration history
stays stable while ensuring Creator role state is consistent across fresh and
existing databases.
In `@backend/creators/models.py`:
- Around line 29-31: `verified_at` on `CommunityPostProof` is only set on first
insert because it uses `auto_now_add=True`, so re-verification leaves the
timestamp stale even when `verify_community_post()` updates the record via
`update_or_create()`. Change `verified_at` to be explicitly assignable (for
example, a normal DateTimeField with a default) and ensure the re-verification
path in `verify_community_post()` updates it whenever `post_url` or `tweet_id`
is refreshed.
In `@backend/creators/views.py`:
- Around line 43-52: Make the legacy join flow idempotent in the join view by
replacing the separate `hasattr(user, 'creator')` /
`Creator.objects.create(user=user)` path with `get_or_create()` on `Creator`, so
concurrent POSTs cannot race into a one-to-one violation. In the same `views.py`
handler, keep the existing “already a community member” response for the
existing record, and only call `update_user_leaderboard_entries(user)` when a
new `Creator` was actually created.
In `@backend/leaderboard/tests/test_stats.py`:
- Around line 279-282: The stats test is asserting creator_count for a POAP-only
user even though no Creator row exists, which can let the API over-report actual
Creator roles. Update the leaderboard stats implementation that backs this test
so creator_count is derived only from real Creator objects, or split the
POAP/community signal into a separately named metric while keeping creator_count
tied to the Creator model.
In `@backend/social_tasks/sorsa_client.py`:
- Around line 165-169: The tweet-info parsing in sorsa_client.py should validate
the user payload before accessing username, because the current
`data.get('user') or {}` pattern can still break if `user` is not a mapping and
raise an AttributeError instead of SorsaError. Update the logic around the
`full_text`/`username` extraction to explicitly check that `data["user"]` is a
dict-like mapping before dereferencing `username`, and raise `SorsaError` on
unexpected shapes so the caller’s retry/verification handling in the Sorsa flow
stays intact.
In `@backend/tally/settings.py`:
- Around line 263-267: The builder journey slug is currently
runtime-configurable in settings while complete_builder_journey depends on that
same value, which can drift from the seeded social task. Make
BUILDER_JOURNEY_TASK_SLUG a fixed constant matching the slug seeded by
0004_seed_builder_star_task, or add startup validation in settings/bootstrapping
to ensure the configured slug exists before requests are served. Use the
existing symbols GITHUB_REPO_TO_STAR, BUILDER_JOURNEY_TASK_SLUG, and
complete_builder_journey to keep the gate and migration in sync.
In `@backend/users/serializers.py`:
- Around line 570-574: Public profile responses from UserSerializer are exposing
in-progress owner-only funnel state via the new has_validator_welcome and
has_community_* fields. Update the public_profile handling in UserSerializer so
these SerializerMethodField booleans are excluded or forced absent for public
access, while preserving them for owner-only/profile edit flows; use the
existing public_profile gating logic around the serializer methods and any field
selection path that controls retrieve and by_address responses.
In `@backend/users/tests/test_community_journey.py`:
- Around line 85-87: The tweet fixtures in test_community_journey should stop
hardcoding `@genlayer` and instead derive the expected mention from
cj.genlayer_handle() so they match the configured GENLAYER_X_HANDLE. Update the
fixture methods used by the success and code_missing paths, such as good_tweet,
to build the tweet text from the helper that the verifier uses, so the
assertions stay correct under non-default handles.
In `@backend/users/tests/test_social_link_rewards.py`:
- Around line 51-60: The test fixture for the GitHub link reward is seeding the
wrong contribution category, so it won’t cover builder-routing behavior. Update
the setup in the social link rewards test where
ContributionType.objects.update_or_create creates community-link-github to use
the builder category instead of self.community, matching the production seed and
keeping the relevant leaderboard/routing assertions meaningful.
In `@backend/users/views.py`:
- Around line 418-445: The builder journey marker write is missing the same
bootstrap step used in start_role_journey(), so Contribution.objects.create()
can fail on a fresh database when no active GlobalLeaderboardMultiplier exists.
Before creating the 0-point builder welcome Contribution, ensure the builder
path looks up or creates the default active GlobalLeaderboardMultiplier (using
the same multiplier bootstrap logic as start_role_journey()) so this lightweight
marker can be saved reliably.
- Around line 495-521: The welcome-start flow in the user view is race-prone
because the `Contribution.objects.filter(...).exists()` check and the subsequent
`GlobalLeaderboardMultiplier.objects.create(...)` /
`Contribution.objects.create(...)` calls are not protected together. Update the
welcome-journey handler to run the idempotency check and both inserts inside a
single transaction, and use a locking/atomic pattern around the relevant
`Contribution` and `GlobalLeaderboardMultiplier` operations so concurrent
requests cannot create duplicate welcome contributions or duplicate default
multiplier rows.
- Around line 979-989: The community completion flow in the user view is
vulnerable to a race between the `hasattr(user, 'creator')` check and
`Creator.objects.create(user=user)`, which can cause duplicate requests to fail
with a 500 instead of returning the existing-member response. Update the
completion logic in the same view to make the `Creator` creation idempotent and
concurrency-safe, using a transaction and handling the one-to-one creation
conflict by reloading the existing `Creator`/user and returning the same 200
“already a community member” path. Keep the existing response shape in the
`update_user_leaderboard_entries` path and ensure the final success branch still
uses `fresh_user`/`self.get_serializer(...)` after a successful or recovered
creation.
- Around line 598-638: The builder completion flow in the relevant view method
should handle concurrent requests safely instead of turning a duplicate creation
into a 500. In the branch that calls Builder.objects.create(user=user), catch
the specific duplicate/one-to-one conflict and treat it as the same idempotent
success path already used when hasattr(user, 'builder') is true, then reuse the
existing serializer/response logic. Keep the existing Builder journey check and
update_user_leaderboard_entries call intact, but make the create path resilient
so near-simultaneous completions return the intended success response rather
than the generic exception handler.
In `@frontend/src/App.svelte`:
- Line 243: The /builders/resources route in App.svelte currently maps directly
to Resources and bypasses the same access control used by the locked sidebar.
Update the route registration so `/builders/resources` is wrapped with
requireRoleForRoute, matching the gating behavior already used for other
builder-only paths and preventing direct navigation from skipping the funnel.
Use the existing Resources route entry in App.svelte and the requireRoleForRoute
helper to keep the sidebar and route protection consistent.
In `@frontend/src/components/funnel/BuilderLanding.svelte`:
- Around line 227-848: The BuilderLanding.svelte styles are implemented as a
large custom stylesheet, but this component should use Tailwind utility classes
instead. Refactor the layout, spacing, typography, colors, and responsive rules
currently defined in the style block into utility classes on the relevant markup
in BuilderLanding, keeping the same visual structure while removing the bespoke
CSS. Focus on the main section/container classes such as builder-landing,
builder-hero, hero-copy, feature-grid, earn-banner, stack-grid, and final-cta
when migrating the styling.
In `@frontend/src/components/funnel/CommunityLanding.svelte`:
- Around line 129-135: The copy in the CommunityLanding section overpromises
points for every contribution, so update the points banner text to match the
actual funnel behavior: the role is granted on completion and only specific
verifiable tasks earn points. Adjust the heading and paragraph in the points
banner content in CommunityLanding.svelte, and make the same wording change for
the later matching copy near the referenced follow-up section so the messaging
is consistent.
- Around line 184-727: The current CommunityLanding component relies on a large
bespoke stylesheet instead of the shared Tailwind system. Refactor the styling
in CommunityLanding.svelte to use Tailwind utility classes in the markup,
removing the custom CSS rules for layout, spacing, colors, shadows, radii, and
typography. Replace hardcoded hex values with the existing primary color scale
and standard spacing tokens, and keep the structure of the hero, feature cards,
points banner, stack cards, and final CTA aligned with their existing component
sections.
In `@frontend/src/components/funnel/journeys/JourneyHeroCard.svelte`:
- Around line 226-232: The progress fill in JourneyHeroCard is forced visible by
the .progress-track span min-width, so a zero-step or 0% journey still appears
partially complete. Update the styling or binding around the progress track span
so the fill can collapse to zero width when progressPercent is 0, while still
preserving the minimum visible size only for non-zero progress states.
- Around line 103-409: The JourneyHeroCard styling is still implemented in a
large standalone <style> block instead of the shared Tailwind utility system.
Move the layout, spacing, typography, colors, borders, gradients, and responsive
behavior into the Svelte markup using Tailwind tokens/classes, and remove the
bespoke CSS rules from JourneyHeroCard.svelte. Use the existing component
structure and unique selectors like journey-hero, hero-content, progress-ring,
hero-role-badge, landing-button, and hero-action as the guide for mapping each
style to Tailwind utilities and shared design tokens.
- Around line 20-22: The JourneyHeroCard component currently exposes heroBadge
and badgeIcon* props, which should be renamed before this API spreads to
callers. Update the JourneyHeroCard Svelte component to use contribution-based
names throughout its surface and internal usage, and make sure any related
references in the component logic are renamed consistently so the new public API
does not contain the banned term.
In `@frontend/src/components/funnel/journeys/JourneyNotice.svelte`:
- Line 27: The visible dismiss “x” in JourneyNotice is inert and misleading
because the aria-hidden control has no interaction behavior. Remove the fake
close control from the notice markup, or replace it with a real dismiss button
in the JourneyNotice component that supports click and keyboard access and is
wired to actual dismiss state handling. Apply the same fix to the related
duplicated notice markup as well.
- Around line 30-89: The JourneyNotice.svelte component is using a
component-scoped stylesheet with hardcoded colors, spacing, and responsive
rules, which should be replaced with Tailwind-based styling. Move the styling
from the <style> block into the component markup by applying Tailwind utility
classes (or existing shared Tailwind component classes) on the journey-notice,
notice-icon, notice-dismiss, and notice-error elements. Keep the same visual
behavior and responsive adjustments, but remove the custom CSS so the component
follows the frontend/**/*.svelte styling guideline.
In `@frontend/src/components/funnel/journeys/JourneyStepRow.svelte`:
- Around line 3-9: The JourneyStepRow component is exposing the banned prop name
in its public API and markup. Rename the `badge` prop in the
JourneyStepRow.svelte component and update its usage throughout the component to
a neutral alternative such as `contributionLabel`, then adjust any callers that
pass `badge` so the new prop name is used consistently. Focus on the
JourneyStepRow component’s prop destructuring and the related rendered label
path so the public API no longer depends on the banned term.
- Around line 124-394: JourneyStepRow currently defines a large private
stylesheet with hardcoded colors, spacing, and typography, which conflicts with
the shared component styling guidelines. Move the row, index, state, action,
skeleton, and responsive styles into Tailwind utility classes used in the
JourneyStepRow Svelte markup, and keep the component aligned with the documented
spacing/color mapping. Use the existing JourneyStepRow structure and state
classes/flags to locate the affected markup, and remove the custom CSS block so
the reusable primitive relies on Tailwind only.
In `@frontend/src/components/funnel/journeys/JourneyUnlockCard.svelte`:
- Around line 30-117: JourneyUnlockCard.svelte still uses component-scoped CSS
with hardcoded sizes/colors, which should be converted to Tailwind utilities per
the Svelte styling guideline. Move the styling from the <style> block into the
markup for the unlock-card layout, lock-badge, unlock-icon, heading, text, and
unlock-label, using Tailwind classes to match the current appearance and
responsive behavior. Keep the existing component structure and identifiers like
unlock-card, lock-badge, unlock-icon, and unlock-label, but remove the custom
stylesheet once the utility classes fully replace it.
In `@frontend/src/components/funnel/RoleLanding.svelte`:
- Around line 24-32: The RoleLanding navigation flow currently swallows errors
from startRoleJourney() and still calls push(journeyPath(role)), which lets
users enter the journey without the durable started marker. Update the handler
in RoleLanding.svelte so that the navigation only happens after
startRoleJourney(role) succeeds; on failure, show a toast notification, keep the
user on the landing page, and still reset starting in the finally block. Use the
existing startRoleJourney, userStore.loadUser, and push/journeyPath(role) flow
as the place to apply the fix.
In `@frontend/src/components/funnel/ValidatorLanding.svelte`:
- Around line 209-829: The ValidatorLanding component still defines the full
layout in a local style block, which conflicts with the Tailwind-only styling
guideline. Remove the custom CSS in ValidatorLanding.svelte and recreate the
same sections using shared Tailwind utility classes and design tokens, keeping
the existing structure/components like role-landing, role-hero, comparison-grid,
stack-grid, and final-cta as the anchors for the refactor.
- Around line 4-6: The validator funnel state check in ValidatorLanding.svelte
is using a nonexistent waitlisted role, so waitlisted users fall through to the
normal CTA. Update the component’s role handling to align with roleFunnelState()
and the funnel contract by treating the waitlist as started (or the equivalent
emitted state), and make the conditional/labels in the validator landing flow
use the existing derived state symbols so the waitlist status renders correctly
instead of “Run a Node.”
In `@frontend/src/components/profile/RoleJourneyCard.svelte`:
- Around line 2-3: The CTA navigation in RoleJourneyCard should use a real
anchor instead of a click handler that calls push(), so update the
RoleJourneyCard.svelte CTA to render a styled <a href={ctaPath}> and remove the
direct push() usage. Keep the existing path helpers like journeyPath and
rolePath to build the target URL, and rely on the global SPA interceptor for
routing so the link preserves normal browser behavior.
- Around line 92-256: The RoleJourneyCard styling has drifted off the shared
Tailwind/design-token system by adding bespoke CSS for layout, colors, borders,
shadows, spacing, and button styling. Refactor the component markup in
RoleJourneyCard.svelte to use Tailwind utility classes and existing
design-token/shadow/border-radius patterns instead of the custom
.journey-reminder stylesheet. Keep the same structure around the
journey-reminder header, status, copy, and journey-reminder__button, but express
the styles through shared utilities so the profile card stays consistent with
other frontend components.
In `@frontend/src/components/ProfileCompletionGuard.svelte`:
- Around line 219-234: The new role selector in ProfileCompletionGuard.svelte is
using component-scoped CSS and inline hex colors instead of the project’s
Tailwind-based styling. Update the role option UI around the ROLE_OPTIONS
rendering and the selectRole-driven button state to use Tailwind utility classes
and existing theme color tokens/classes, and remove the custom CSS/inline color
styling from this selector so it matches the coding guidelines and the rest of
the component.
- Around line 22-28: The onboarding redirect in ProfileCompletionGuard should
not trust sessionStorage.onboardingRole directly, since it may be stale or
invalid. Normalize the stored value against ROLE_OPTIONS before setting
selectedRole and before calling rolePath(selectedRole), so the guard always
redirects to a known role instead of falling back to /. Update the logic around
the state initialization and the redirect path selection to use the validated
role value consistently.
In `@frontend/src/components/Sidebar.svelte`:
- Around line 128-132: The Sidebar link behavior still relies on openRoleSection
and onclick-based navigate calls, which leaves the locked subsection URL in href
and breaks open-in-new-tab/copy-link behavior. Update the affected anchors in
Sidebar.svelte to compute the final destination directly in href using the same
rolePath/journeyPath logic currently inside openRoleSection, and let the global
interceptor handle SPA navigation instead of invoking navigate from click
handlers.
- Around line 67-91: The community journey lock-state fetch in
loadCommunityJourneyLockState() can apply a stale async response after the
authenticated user or key has changed. Update Sidebar.svelte to associate each
request with the current communityJourneyKey (or equivalent request token) and
only write communityJourneyComplete when the returned response still matches the
latest key. Use the existing $effect block and loadCommunityJourneyLockState()
to guard against out-of-date responses before mutating state.
In `@frontend/src/components/SocialLink.svelte`:
- Around line 117-128: The GitHub linking flow in SocialLink.svelte is
swallowing the failure from journeyAPI.linkGithubAccount(), which can leave the
journey marker missing while still showing success. Update the then-handler
around userPromise so the GitHub reward path surfaces a retryable warning/error
via a toast or similar user-facing message instead of an empty catch, and keep
the successful OAuth link path intact by only treating the reward step as
best-effort. Use the existing platform === 'github' and !wasRefreshing branch,
and make sure the fallback for the final user state still works when the reward
call fails.
In `@frontend/src/routes/BuilderJourney.svelte`:
- Around line 733-1131: The BuilderJourney route still uses a large custom
<style> block instead of Tailwind utilities. Move the styling for the
.journey-page layout and its sections like .steps-card, .task-panel,
.network-item, .completion-panel, and .unlock-grid into the Svelte markup using
Tailwind class names, including responsive behavior now handled by the media
queries. Keep the component structure intact while replacing the hardcoded
spacing, colors, borders, and hover states with equivalent utility classes.
- Around line 23-37: The network setup in BuilderJourney.svelte is treating
Bradbury and Asimov as separate additions even though both use the same chainId,
which lets checkNetworks() satisfy both steps from one active chain. Fix this by
either assigning distinct chain IDs to BRADBURY_NETWORK and ASIMOV_NETWORK or
merging them into a single network/step, and update the related network-step
logic so each step maps to a truly unique chain.
- Around line 180-189: The BuilderJourney initialization flow is swallowing
errors in the onMount startBuilderJourney call, so replace the empty catch with
proper error handling. Update the onMount logic in BuilderJourney.svelte to
surface failures through a user-facing toast notification, while keeping the
existing userStore update/loadUser success path intact and using the
journeyAPI.startBuilderJourney symbol to locate the fix.
In `@frontend/src/routes/CommunityJourney.svelte`:
- Around line 79-82: The community journey initialization in onMount currently
swallows failures from startRoleJourney('community'), so handle this API call
with explicit try-catch instead of an empty catch. In CommunityJourney.svelte,
keep the userStore.loadUser refresh on success, and on failure surface a
user-facing toast notification with the error message so users know the journey
setup did not complete.
- Around line 478-784: The CommunityJourney route still defines a large
component-scoped <style> block, which violates the Tailwind-only styling
guideline. Move the styles from the JourneyPage layout and its sections (for
example journey-page, task-panel, x-post-panel, share-box, verify-card,
completion-panel, landing-button, unlock-section, and unlock-grid) into Tailwind
utility classes directly in the Svelte markup, keeping the same responsive
behavior and state styling via utility variants instead of custom CSS.
- Around line 174-183: The copy fallback in CommunityJourney.svelte currently
assumes document.execCommand('copy') always succeeds, so update the logic around
the textarea-based copy flow to check the return value from execCommand before
calling showSuccess. If it returns true, keep the success message; if it returns
false, surface an error instead, using the same copy helper/handler that
performs the fallback.
In `@frontend/src/routes/Profile.svelte`:
- Around line 123-140: The `communityInProgress` derived state in
`Profile.svelte` is incorrectly treating existing creators as in progress while
`journeyAPI.communityJourney()` is still loading. Update the
`participant?.creator` fallback branch so it only applies after the community
journey response has loaded or otherwise defaults creators to not in-progress
until `communityJourney` is resolved, keeping `ProfileHeader` from showing the
grey in-progress state prematurely.
In `@frontend/src/routes/ValidatorWaitlist.svelte`:
- Around line 90-92: The waitlist submission failure in ValidatorWaitlist.svelte
only updates inline error state and does not surface the API error through the
toast system. In the existing try-catch around the waitlist join flow, add a
toast notification in the catch block alongside the current error assignment and
loading reset so users get a visible failure message. Use the route’s waitlist
submission logic and its existing error handling path to keep the user-facing
feedback consistent with the app guideline.
- Around line 210-538: The ValidatorWaitlist.svelte route still uses a large
custom style block instead of Tailwind utilities, which violates the frontend
styling guideline. Replace the CSS for the journey layout by moving the styling
into the Svelte markup using Tailwind classes on the relevant elements such as
journey-page, steps-card, step-check-panel, graduation-grid, graduation-card,
contact-card, and landing-button. Preserve the current responsive behavior and
visual hierarchy by translating the media-query-driven layout into Tailwind
responsive variants rather than keeping custom CSS.
- Around line 85-92: The join flow in ValidatorWaitlist.svelte treats a failure
in sessionStorage.setItem as if startValidatorJourney failed, which incorrectly
shows “Failed to join waitlist” after the backend call already succeeded. Update
the success path around startValidatorJourney, userStore.loadUser, and replace
so the persistence write for journeySuccess is isolated from the mutation error
handling, and handle any storage exception without entering the main catch or
setting the join-failed error state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3254f750-ef8d-44a6-8fe1-d9f954d7b3b9
📒 Files selected for processing (53)
CHANGELOG.mdbackend/CLAUDE.mdbackend/community_xp/services.pybackend/community_xp/tests/test_mee6_sync.pybackend/contributions/migrations/0073_create_community_link_github.pybackend/contributions/tests/test_is_submittable.pybackend/creators/community_journey.pybackend/creators/migrations/0002_backfill_community_members.pybackend/creators/migrations/0003_communitypostproof.pybackend/creators/models.pybackend/creators/utils.pybackend/creators/views.pybackend/leaderboard/models.pybackend/leaderboard/tests/test_stats.pybackend/poaps/management/commands/import_poap_archive.pybackend/poaps/signals.pybackend/social_tasks/migrations/0004_seed_builder_star_task.pybackend/social_tasks/sorsa_client.pybackend/tally/settings.pybackend/users/serializers.pybackend/users/tests/test_builder_journey.pybackend/users/tests/test_community_journey.pybackend/users/tests/test_social_link_rewards.pybackend/users/views.pyfrontend/src/App.sveltefrontend/src/components/ProfileCompletionGuard.sveltefrontend/src/components/Sidebar.sveltefrontend/src/components/SocialLink.sveltefrontend/src/components/WhatsNewDialog.sveltefrontend/src/components/funnel/BuilderLanding.sveltefrontend/src/components/funnel/CommunityLanding.sveltefrontend/src/components/funnel/RoleFunnel.sveltefrontend/src/components/funnel/RoleLanding.sveltefrontend/src/components/funnel/ValidatorLanding.sveltefrontend/src/components/funnel/journeys/JourneyHeroCard.sveltefrontend/src/components/funnel/journeys/JourneyNotice.sveltefrontend/src/components/funnel/journeys/JourneyStepRow.sveltefrontend/src/components/funnel/journeys/JourneyUnlockCard.sveltefrontend/src/components/profile/CommunityProgressJourney.sveltefrontend/src/components/profile/CommunityView.sveltefrontend/src/components/profile/JourneyActions.sveltefrontend/src/components/profile/ProfileHeader.sveltefrontend/src/components/profile/ProgressJourney.sveltefrontend/src/components/profile/RoleJourneyCard.sveltefrontend/src/components/social-tasks/SocialTaskCard.sveltefrontend/src/lib/api.jsfrontend/src/lib/auth.jsfrontend/src/lib/roleState.jsfrontend/src/routes/BuilderJourney.sveltefrontend/src/routes/CommunityJourney.sveltefrontend/src/routes/Profile.sveltefrontend/src/routes/ValidatorWaitlist.sveltefrontend/src/tests/roleState.test.js
💤 Files with no reviewable changes (8)
- frontend/src/components/profile/JourneyActions.svelte
- frontend/src/components/profile/ProgressJourney.svelte
- backend/creators/utils.py
- backend/poaps/signals.py
- backend/poaps/management/commands/import_poap_archive.py
- frontend/src/components/profile/CommunityProgressJourney.svelte
- backend/leaderboard/models.py
- frontend/src/components/profile/CommunityView.svelte
| builder_category = Category.objects.filter(slug='builder').first() | ||
| ContributionType.objects.update_or_create( | ||
| slug='community-link-github', | ||
| defaults={ | ||
| 'name': 'Link GitHub Account', | ||
| 'description': 'Linked your GitHub account to your GenLayer profile', | ||
| 'category': builder_category, | ||
| 'min_points': 25, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fail closed if the builder category is missing.
Category.objects.filter(...).first() lets this migration seed community-link-github with category=None. Because the foreign key is nullable, the migration would succeed while silently creating a misclassified contribution type that no longer behaves like a builder contribution.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/contributions/migrations/0073_create_community_link_github.py` around
lines 21 - 28, The migration for ContributionType creation currently uses
Category.objects.filter(...).first(), which can silently set the category to
none if the builder category is missing. Update the migration logic around
builder_category and ContributionType.objects.update_or_create to fail closed by
explicitly checking that the builder Category exists before seeding
community-link-github, and raise an error if it does not. Keep the fix localized
to the migration so the Link GitHub Account contribution type is only created
when the builder category is present.
| def post_matches(full_text: str, user): | ||
| """Whether the tweet text contains the user's code and @mentions GenLayer. | ||
| Returns (ok, error_code).""" | ||
| text = (full_text or '').lower() | ||
| if verification_code(user).lower() not in text: | ||
| return False, 'code_missing' | ||
| if f'@{genlayer_handle()}' not in text: | ||
| return False, 'tag_missing' |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Require an exact @handle mention for step 5.
The current substring check accepts handles like @genlayerlabs when GENLAYER_X_HANDLE is genlayer, because @genlayer is contained inside the longer mention. That weakens the journey gate and can award Creator without tagging the intended account.
Suggested fix
def post_matches(full_text: str, user):
"""Whether the tweet text contains the user's code and `@mentions` GenLayer.
Returns (ok, error_code)."""
text = (full_text or '').lower()
if verification_code(user).lower() not in text:
return False, 'code_missing'
- if f'@{genlayer_handle()}' not in text:
+ handle_re = re.compile(
+ rf'(^|[^a-z0-9_])@{re.escape(genlayer_handle())}(?![a-z0-9_])'
+ )
+ if not handle_re.search(text):
return False, 'tag_missing'
return True, None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def post_matches(full_text: str, user): | |
| """Whether the tweet text contains the user's code and @mentions GenLayer. | |
| Returns (ok, error_code).""" | |
| text = (full_text or '').lower() | |
| if verification_code(user).lower() not in text: | |
| return False, 'code_missing' | |
| if f'@{genlayer_handle()}' not in text: | |
| return False, 'tag_missing' | |
| def post_matches(full_text: str, user): | |
| """Whether the tweet text contains the user's code and `@mentions` GenLayer. | |
| Returns (ok, error_code).""" | |
| text = (full_text or '').lower() | |
| if verification_code(user).lower() not in text: | |
| return False, 'code_missing' | |
| handle_re = re.compile( | |
| rf'(^|[^a-z0-9_])@{re.escape(genlayer_handle())}(?![a-z0-9_])' | |
| ) | |
| if not handle_re.search(text): | |
| return False, 'tag_missing' |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/creators/community_journey.py` around lines 69 - 76, The mention
check in post_matches is too permissive because it uses a substring match
against genlayer_handle(), which can accept longer handles like `@genlayerlabs`.
Update the step 5 validation in post_matches to require an exact `@handle` mention
for the intended account, using a boundary-aware match or tokenized mention
parsing so only the precise handle from genlayer_handle() passes while near
matches fail.
| def step_states(user) -> dict: | ||
| return { | ||
| 'link_x': _has_contribution(user, LINK_X_SLUG), | ||
| 'link_discord': _has_contribution(user, LINK_DISCORD_SLUG), | ||
| 'follow_x': _has_task_completion(user, FOLLOW_TASK_SLUG), | ||
| 'join_discord': _has_task_completion(user, JOIN_DISCORD_TASK_SLUG), | ||
| 'x_post': hasattr(user, 'community_post_proof'), | ||
| } | ||
|
|
||
|
|
||
| def journey_status(user) -> dict: | ||
| states = step_states(user) | ||
| started = is_started(user) | ||
| missing_steps = [key for key, done in states.items() if not done] | ||
| proof = getattr(user, 'community_post_proof', None) | ||
| return { | ||
| 'started': started, | ||
| 'steps': { | ||
| 'link_x': {'done': states['link_x']}, | ||
| 'link_discord': {'done': states['link_discord']}, | ||
| 'follow_x': {'done': states['follow_x']}, | ||
| 'join_discord': {'done': states['join_discord']}, | ||
| 'x_post': { | ||
| 'done': states['x_post'], | ||
| 'verification_code': verification_code(user), | ||
| 'share_text': share_text(user), | ||
| 'intent_url': intent_url(user), | ||
| 'post_url': proof.post_url if proof else None, | ||
| }, | ||
| }, | ||
| 'missing_steps': missing_steps, | ||
| 'complete': started and not missing_steps, | ||
| 'is_member': hasattr(user, 'creator') and started and not missing_steps, | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Don't mark step 5 complete from proof existence alone.
x_post flips to done as soon as community_post_proof exists. If someone verifies with one linked X account and then relinks to another, journey_status() still reports completion, and the legacy join route in backend/creators/views.py:13-58 will grant Creator without re-checking that the stored proof still belongs to the currently linked account. Compare the current linked handle against the proof URL/verified author before returning x_post: true.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/creators/community_journey.py` around lines 94 - 127, The x_post step
in step_states and journey_status is currently marked done whenever
community_post_proof exists, which can leave Creator completion stale after an X
relink. Update the x_post check to validate that the stored proof still matches
the currently linked X account before returning done, using the current linked
handle together with the proof URL/verified author. Keep the completion logic in
journey_status consistent so the legacy join route in views does not treat an
outdated proof as valid.
| # Creator rows are granted only through the community journey completion | ||
| # endpoint. Keep this historical migration as a no-op for fresh databases. | ||
| return |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Don’t rewrite the historical role backfill in place.
Existing databases that already ran this migration will keep the auto-created Creator rows, while fresh databases will not create them, producing environment-dependent role state. Add a new follow-up migration or audited cleanup path to reconcile previously auto-granted rows instead of changing this migration’s historical behavior. This conflicts with the PR objective that Creator roles are granted only through journey completion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/creators/migrations/0002_backfill_community_members.py` around lines
5 - 7, The historical backfill in 0002_backfill_community_members must remain
unchanged for databases that already ran it, so don’t turn it into a no-op or
alter its past behavior. Instead, add a new follow-up migration or audited
cleanup path alongside 0002_backfill_community_members to reconcile previously
auto-created Creator rows and align existing environments with the new
journey-completion-only rule. Keep the fix scoped so the migration history stays
stable while ensuring Creator role state is consistent across fresh and existing
databases.
| post_url = models.URLField() | ||
| tweet_id = models.CharField(max_length=40, blank=True) | ||
| verified_at = models.DateTimeField(auto_now_add=True) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Make verified_at reflect the latest successful verification.
verify_community_post() updates an existing CommunityPostProof with update_or_create(), but auto_now_add=True only sets this field on the first insert. After a user re-verifies a corrected post URL, post_url/tweet_id change while verified_at stays stale. If this timestamp is meant to describe the current proof, switch it to an explicit default and update it in the re-verification path too.
🧰 Tools
🪛 ast-grep (0.44.0)
[info] 29-29: use help_text to document model columns
Context: models.CharField(max_length=40, blank=True)
Note: [CWE-710] Improper Adherence to Coding Standards.
(model-help-text)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/creators/models.py` around lines 29 - 31, `verified_at` on
`CommunityPostProof` is only set on first insert because it uses
`auto_now_add=True`, so re-verification leaves the timestamp stale even when
`verify_community_post()` updates the record via `update_or_create()`. Change
`verified_at` to be explicitly assignable (for example, a normal DateTimeField
with a default) and ensure the re-verification path in `verify_community_post()`
updates it whenever `post_url` or `tweet_id` is refreshed.
| async function loadCommunityJourneyLockState() { | ||
| try { | ||
| const res = await journeyAPI.communityJourney(); | ||
| communityJourneyComplete = Boolean(res.data?.is_member && res.data?.complete); | ||
| } catch { | ||
| communityJourneyComplete = false; | ||
| } | ||
| } | ||
|
|
||
| $effect(() => { | ||
| const user = $userStore.user; | ||
| const key = $authState.isAuthenticated | ||
| ? `${user?.id || user?.address || ''}:${user?.creator ? 1 : 0}:${user?.has_community_welcome ? 1 : 0}:${user?.has_community_link_x ? 1 : 0}:${user?.has_community_link_discord ? 1 : 0}` | ||
| : ''; | ||
|
|
||
| if (key === communityJourneyKey) return; | ||
| communityJourneyKey = key; | ||
|
|
||
| if (!$authState.isAuthenticated || !user?.creator) { | ||
| communityJourneyComplete = false; | ||
| return; | ||
| } | ||
|
|
||
| loadCommunityJourneyLockState(); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Guard community lock-state responses by request key.
loadCommunityJourneyLockState() writes communityJourneyComplete after an async request without confirming it still matches the current user/auth key. A stale response can unlock or lock the next user’s sidebar.
Proposed fix
- async function loadCommunityJourneyLockState() {
+ async function loadCommunityJourneyLockState(key) {
try {
const res = await journeyAPI.communityJourney();
+ if (key !== communityJourneyKey) return;
communityJourneyComplete = Boolean(res.data?.is_member && res.data?.complete);
} catch {
+ if (key !== communityJourneyKey) return;
communityJourneyComplete = false;
}
}
@@
- loadCommunityJourneyLockState();
+ loadCommunityJourneyLockState(key);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function loadCommunityJourneyLockState() { | |
| try { | |
| const res = await journeyAPI.communityJourney(); | |
| communityJourneyComplete = Boolean(res.data?.is_member && res.data?.complete); | |
| } catch { | |
| communityJourneyComplete = false; | |
| } | |
| } | |
| $effect(() => { | |
| const user = $userStore.user; | |
| const key = $authState.isAuthenticated | |
| ? `${user?.id || user?.address || ''}:${user?.creator ? 1 : 0}:${user?.has_community_welcome ? 1 : 0}:${user?.has_community_link_x ? 1 : 0}:${user?.has_community_link_discord ? 1 : 0}` | |
| : ''; | |
| if (key === communityJourneyKey) return; | |
| communityJourneyKey = key; | |
| if (!$authState.isAuthenticated || !user?.creator) { | |
| communityJourneyComplete = false; | |
| return; | |
| } | |
| loadCommunityJourneyLockState(); | |
| }); | |
| async function loadCommunityJourneyLockState(key) { | |
| try { | |
| const res = await journeyAPI.communityJourney(); | |
| if (key !== communityJourneyKey) return; | |
| communityJourneyComplete = Boolean(res.data?.is_member && res.data?.complete); | |
| } catch { | |
| if (key !== communityJourneyKey) return; | |
| communityJourneyComplete = false; | |
| } | |
| } | |
| $effect(() => { | |
| const user = $userStore.user; | |
| const key = $authState.isAuthenticated | |
| ? `${user?.id || user?.address || ''}:${user?.creator ? 1 : 0}:${user?.has_community_welcome ? 1 : 0}:${user?.has_community_link_x ? 1 : 0}:${user?.has_community_link_discord ? 1 : 0}` | |
| : ''; | |
| if (key === communityJourneyKey) return; | |
| communityJourneyKey = key; | |
| if (!$authState.isAuthenticated || !user?.creator) { | |
| communityJourneyComplete = false; | |
| return; | |
| } | |
| loadCommunityJourneyLockState(key); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/Sidebar.svelte` around lines 67 - 91, The community
journey lock-state fetch in loadCommunityJourneyLockState() can apply a stale
async response after the authenticated user or key has changed. Update
Sidebar.svelte to associate each request with the current communityJourneyKey
(or equivalent request token) and only write communityJourneyComplete when the
returned response still matches the latest key. Use the existing $effect block
and loadCommunityJourneyLockState() to guard against out-of-date responses
before mutating state.
| // Clicking a locked role subsection nudges the user to that role's funnel | ||
| // instead of the (route-gated) subsection. | ||
| function openRoleSection(path, category) { | ||
| navigate(isRoleLocked(category) ? (category === 'community' ? journeyPath(category) : rolePath(category)) : path); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use computed hrefs instead of click-handler navigation.
These links still expose the locked subsection in href and only redirect via onclick, so open-in-new-tab/copy-link bypasses the intended funnel nudge. Compute the destination in href and let the global link interceptor handle SPA navigation.
Proposed pattern
- function openRoleSection(path, category) {
- navigate(isRoleLocked(category) ? (category === 'community' ? journeyPath(category) : rolePath(category)) : path);
+ function roleSectionHref(path, category) {
+ return isRoleLocked(category)
+ ? (category === 'community' ? journeyPath(category) : rolePath(category))
+ : path;
}- href="/builders/contributions"
- onclick={(e) => { e.preventDefault(); openRoleSection('/builders/contributions', 'builder'); }}
+ href={roleSectionHref('/builders/contributions', 'builder')}As per coding guidelines: “Use plain <a href="/path"> anchors for in-app navigation … instead of calling push() in click handlers.”
Also applies to: 309-331, 361-396, 426-448, 782-804, 827-862, 885-907
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/Sidebar.svelte` around lines 128 - 132, The Sidebar
link behavior still relies on openRoleSection and onclick-based navigate calls,
which leaves the locked subsection URL in href and breaks
open-in-new-tab/copy-link behavior. Update the affected anchors in
Sidebar.svelte to compute the final destination directly in href using the same
rolePath/journeyPath logic currently inside openRoleSection, and let the global
interceptor handle SPA navigation instead of invoking navigate from click
handlers.
Source: Coding guidelines
| userPromise.then(async (resolvedUser) => { | ||
| let finalUser = resolvedUser; | ||
| // Linking GitHub counts as a contribution (like X / Discord). Award it | ||
| // here so it fires wherever GitHub is linked; idempotent + best-effort, | ||
| // so a failed reward never blocks the successful link. | ||
| if (platform === 'github' && !wasRefreshing) { | ||
| try { | ||
| await journeyAPI.linkGithubAccount(); | ||
| finalUser = await getCurrentUser(); | ||
| } catch { | ||
| // Link still succeeded; the reward is retried next time it's called. | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Don’t silently drop the GitHub journey marker.
If linkGithubAccount() fails, the OAuth link succeeds but the contribution/journey marker can remain missing, and the user only sees a success path. Surface a retryable error/warning instead of swallowing it.
Proposed fix
userPromise.then(async (resolvedUser) => {
let finalUser = resolvedUser;
+ let rewardFailed = false;
@@
try {
await journeyAPI.linkGithubAccount();
finalUser = await getCurrentUser();
} catch {
- // Link still succeeded; the reward is retried next time it's called.
+ rewardFailed = true;
}
}
if (shouldToast) {
+ if (rewardFailed) {
+ showError('GitHub account linked, but we could not record the contribution. Please try again.');
+ } else {
const connData = finalUser?.[`${platform}_connection`];
@@
showSuccess(`${platformLabel} account linked successfully!${username ? ` (@${username})` : ''}`);
}
+ }
}As per coding guidelines: “Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications.”
Also applies to: 130-144
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SocialLink.svelte` around lines 117 - 128, The GitHub
linking flow in SocialLink.svelte is swallowing the failure from
journeyAPI.linkGithubAccount(), which can leave the journey marker missing while
still showing success. Update the then-handler around userPromise so the GitHub
reward path surfaces a retryable warning/error via a toast or similar
user-facing message instead of an empty catch, and keep the successful OAuth
link path intact by only treating the reward step as best-effort. Use the
existing platform === 'github' and !wasRefreshing branch, and make sure the
fallback for the final user state still works when the reward call fails.
Source: Coding guidelines
| onMount(() => { | ||
| if (!user?.has_community_welcome) { | ||
| journeyAPI.startRoleJourney('community').then(() => userStore.loadUser?.()).catch(() => {}); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Surface community journey initialization failures.
startRoleJourney('community') is an authenticated state-changing call, but errors are swallowed. Users can proceed with missing journey state and no toast.
As per coding guidelines, "frontend/**/{components,routes}/**/*.{ts,tsx,js,svelte}: Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/CommunityJourney.svelte` around lines 79 - 82, The
community journey initialization in onMount currently swallows failures from
startRoleJourney('community'), so handle this API call with explicit try-catch
instead of an empty catch. In CommunityJourney.svelte, keep the
userStore.loadUser refresh on success, and on failure surface a user-facing
toast notification with the error message so users know the journey setup did
not complete.
Source: Coding guidelines
| let communityJourneyComplete = $derived( | ||
| Boolean(communityJourney?.complete && communityJourney?.is_member), | ||
| ); | ||
| let communityJourneyHasLocalSignal = $derived( | ||
| Boolean( | ||
| participant?.has_community_welcome || | ||
| participant?.has_community_link_x || | ||
| participant?.has_community_link_discord, | ||
| ), | ||
| ); | ||
| let communityInProgress = $derived( | ||
| isOwnProfile && | ||
| (hasStartedJourney(participant, "community") || | ||
| communityJourneyHasLocalSignal || | ||
| (Boolean(participant?.creator) && | ||
| !communityJourneyComplete && | ||
| !communityJourneyCheckFailed)), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don't treat existing Creators as "journey in progress" while the status fetch is still loading.
Before journeyAPI.communityJourney() resolves, communityJourneyComplete is false, so the participant?.creator fallback makes communityInProgress true for every owner who already has the Creator role. That temporarily hides the normal community section and swaps the earned Community state in ProfileHeader for the grey in-progress version. Gate that branch on a loaded journey response, or default existing creators to not in-progress until the API proves otherwise.
Also applies to: 166-166, 637-640
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/Profile.svelte` around lines 123 - 140, The
`communityInProgress` derived state in `Profile.svelte` is incorrectly treating
existing creators as in progress while `journeyAPI.communityJourney()` is still
loading. Update the `participant?.creator` fallback branch so it only applies
after the community journey response has loaded or otherwise defaults creators
to not in-progress until `communityJourney` is resolved, keeping `ProfileHeader`
from showing the grey in-progress state prematurely.
| const BRADBURY_NETWORK = { | ||
| chainId: '0x107D', | ||
| chainName: 'GenLayer Bradbury', | ||
| nativeCurrency: { name: 'GEN', symbol: 'GEN', decimals: 18 }, | ||
| rpcUrls: ['https://rpc-bradbury.genlayer.com'], | ||
| blockExplorerUrls: ['https://explorer-bradbury.genlayer.com'], | ||
| }; | ||
|
|
||
| const ASIMOV_NETWORK = { | ||
| chainId: '0x107D', | ||
| chainName: 'GenLayer Asimov', | ||
| nativeCurrency: { name: 'GEN', symbol: 'GEN', decimals: 18 }, | ||
| rpcUrls: ['https://rpc-asimov.genlayer.com'], | ||
| blockExplorerUrls: ['https://explorer-asimov.genlayer.com'], | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
from pathlib import Path
import re
path = Path("frontend/src/routes/BuilderJourney.svelte")
text = path.read_text()
for name in ("BRADBURY_NETWORK", "ASIMOV_NETWORK", "STUDIO_NETWORK"):
m = re.search(rf"const {name}\s*=\s*\{{.*?chainId:\s*'([^']+)'", text, re.S)
print(f"{name}: {m.group(1) if m else 'not found'}")
PYRepository: genlayer-foundation/points
Length of output: 235
Do not treat Bradbury and Asimov as separate network additions while they share 0x107D.
checkNetworks() marks both steps complete from a single active chain, so the Asimov step can be satisfied without adding a distinct Asimov RPC entry. Give them different chain IDs or collapse them into one step.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/BuilderJourney.svelte` around lines 23 - 37, The network
setup in BuilderJourney.svelte is treating Bradbury and Asimov as separate
additions even though both use the same chainId, which lets checkNetworks()
satisfy both steps from one active chain. Fix this by either assigning distinct
chain IDs to BRADBURY_NETWORK and ASIMOV_NETWORK or merging them into a single
network/step, and update the related network-step logic so each step maps to a
truly unique chain.
| onMount(() => { | ||
| if (!user?.has_builder_welcome && !user?.builder) { | ||
| journeyAPI | ||
| .startBuilderJourney() | ||
| .then((res) => { | ||
| if (res.data?.user) userStore.updateUser(res.data.user); | ||
| else userStore.loadUser?.(); | ||
| }) | ||
| .catch(() => {}); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Surface builder journey initialization failures.
This authenticated API call currently swallows failures, so users can continue without the server-side journey marker being created and get no actionable feedback.
As per coding guidelines, "frontend/**/{components,routes}/**/*.{ts,tsx,js,svelte}: Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications."
Proposed fix
onMount(() => {
- if (!user?.has_builder_welcome && !user?.builder) {
- journeyAPI
- .startBuilderJourney()
- .then((res) => {
- if (res.data?.user) userStore.updateUser(res.data.user);
- else userStore.loadUser?.();
- })
- .catch(() => {});
- }
+ startBuilderJourney();
loadTasks({ showLoading: true });
});
+
+ async function startBuilderJourney() {
+ if (user?.has_builder_welcome || user?.builder) return;
+ try {
+ const res = await journeyAPI.startBuilderJourney();
+ if (res.data?.user) userStore.updateUser(res.data.user);
+ else await userStore.loadUser?.();
+ } catch {
+ showWarning('Could not start your builder journey. Try refreshing in a moment.');
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onMount(() => { | |
| if (!user?.has_builder_welcome && !user?.builder) { | |
| journeyAPI | |
| .startBuilderJourney() | |
| .then((res) => { | |
| if (res.data?.user) userStore.updateUser(res.data.user); | |
| else userStore.loadUser?.(); | |
| }) | |
| .catch(() => {}); | |
| } | |
| onMount(() => { | |
| startBuilderJourney(); | |
| loadTasks({ showLoading: true }); | |
| }); | |
| async function startBuilderJourney() { | |
| if (user?.has_builder_welcome || user?.builder) return; | |
| try { | |
| const res = await journeyAPI.startBuilderJourney(); | |
| if (res.data?.user) userStore.updateUser(res.data.user); | |
| else await userStore.loadUser?.(); | |
| } catch { | |
| showWarning('Could not start your builder journey. Try refreshing in a moment.'); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/BuilderJourney.svelte` around lines 180 - 189, The
BuilderJourney initialization flow is swallowing errors in the onMount
startBuilderJourney call, so replace the empty catch with proper error handling.
Update the onMount logic in BuilderJourney.svelte to surface failures through a
user-facing toast notification, while keeping the existing userStore
update/loadUser success path intact and using the journeyAPI.startBuilderJourney
symbol to locate the fix.
Source: Coding guidelines
| <style> | ||
| .journey-page { | ||
| --role-accent: #ee8521; | ||
| --role-accent-hover: #d97518; | ||
| --journey-active-bg: #fff7ec; | ||
| --journey-black: #131214; | ||
| --journey-border: #ededed; | ||
| --journey-muted: #909090; | ||
| --journey-hero-bg: linear-gradient(163deg, #fff 40%, #fff7ec 96%); | ||
| --journey-hero-border: #f3e4cb; | ||
| --journey-hero-glow: rgba(238, 133, 33, 0.16); | ||
| --journey-points-bg: #fdf3e6; | ||
| --builder-orange-gradient: linear-gradient(135deg, #fff 0%, #fff7ec 52%, #ffe3bd 100%); | ||
| --builder-orange-gradient-hover: linear-gradient(135deg, #fffaf3 0%, #fff0dc 52%, #ffd7a3 100%); | ||
| --builder-orange-gradient-ink: #c86513; | ||
| --builder-orange-gradient-border: #f1d8b7; | ||
| --journey-complete-gradient: var(--builder-orange-gradient); | ||
| --journey-complete-border: var(--builder-orange-gradient-border); | ||
| --journey-complete-color: var(--builder-orange-gradient-ink); | ||
| --journey-complete-shadow: rgba(238, 133, 33, 0.14); | ||
| color: #000; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| margin: 0 auto; | ||
| max-width: 940px; | ||
| padding: 20px 12px 80px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .journey-page :global(*) { | ||
| letter-spacing: 0; | ||
| } | ||
|
|
||
| .steps-card { | ||
| background: #fff; | ||
| border: 1px solid var(--journey-border); | ||
| border-radius: 14px; | ||
| overflow: hidden; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .step-block { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .task-panel { | ||
| background: linear-gradient(90deg, var(--journey-active-bg) 0%, #fff 82%); | ||
| border-top: 1px solid #f0f0f0; | ||
| display: grid; | ||
| gap: 16px; | ||
| grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); | ||
| padding: 16px 18px 18px 58px; | ||
| } | ||
|
|
||
| .compact-panel { | ||
| align-items: center; | ||
| grid-template-columns: minmax(0, 1fr) auto; | ||
| } | ||
|
|
||
| .task-panel-copy p, | ||
| .task-panel-error p { | ||
| color: #737373; | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| line-height: 22px; | ||
| margin: 0; | ||
| max-width: 430px; | ||
| } | ||
|
|
||
| .task-card-frame, | ||
| .social-link-frame { | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn), | ||
| .social-link-frame :global(.social-connected-row) { | ||
| border-radius: 20px; | ||
| font-family: var(--font-body); | ||
| min-height: 40px; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn) { | ||
| background: var(--journey-black) !important; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn:hover:not(:disabled)) { | ||
| background: #2a292c !important; | ||
| } | ||
|
|
||
| .task-panel-error { | ||
| align-items: center; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| } | ||
|
|
||
| .panel-actions { | ||
| align-items: center; | ||
| display: flex; | ||
| gap: 10px; | ||
| justify-content: flex-end; | ||
| } | ||
|
|
||
| .network-list { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 8px; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .network-item { | ||
| align-items: center; | ||
| background: #fff; | ||
| border: 1px solid #f0f0f0; | ||
| border-radius: 8px; | ||
| display: grid; | ||
| gap: 10px; | ||
| grid-template-columns: 24px minmax(0, 1fr) auto; | ||
| min-height: 52px; | ||
| padding: 9px 10px; | ||
| } | ||
|
|
||
| .network-item-done { | ||
| background: linear-gradient(90deg, rgba(238, 133, 33, 0.12) 0%, #fff 86%); | ||
| border-color: #f0d7b7; | ||
| } | ||
|
|
||
| .network-check { | ||
| align-items: center; | ||
| border: 1px solid #e6e6e6; | ||
| border-radius: 12px; | ||
| color: var(--role-accent); | ||
| display: inline-flex; | ||
| height: 24px; | ||
| justify-content: center; | ||
| width: 24px; | ||
| } | ||
|
|
||
| .network-item-done .network-check { | ||
| background: var(--builder-orange-gradient); | ||
| border-color: var(--builder-orange-gradient-border); | ||
| box-shadow: 0 7px 15px rgba(238, 133, 33, 0.14); | ||
| color: var(--builder-orange-gradient-ink); | ||
| } | ||
|
|
||
| .network-check svg { | ||
| height: 14px; | ||
| width: 14px; | ||
| } | ||
|
|
||
| .network-check span { | ||
| background: #d6d6d6; | ||
| border-radius: 999px; | ||
| height: 6px; | ||
| width: 6px; | ||
| } | ||
|
|
||
| .network-copy { | ||
| display: flex; | ||
| flex-direction: column; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .network-copy strong { | ||
| color: var(--journey-black); | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| line-height: 20px; | ||
| } | ||
|
|
||
| .network-copy small { | ||
| color: #737373; | ||
| font-family: var(--font-body); | ||
| font-size: 12px; | ||
| line-height: 17px; | ||
| } | ||
|
|
||
| .network-item em { | ||
| color: var(--role-accent); | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| font-style: normal; | ||
| letter-spacing: 1.1px !important; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .network-button { | ||
| align-items: center; | ||
| background: #fff; | ||
| border: 1px solid var(--journey-black); | ||
| border-radius: 16px; | ||
| color: var(--journey-black); | ||
| display: inline-flex; | ||
| font-family: var(--font-body); | ||
| font-size: 13px; | ||
| font-weight: 500; | ||
| height: 32px; | ||
| justify-content: center; | ||
| padding: 0 13px; | ||
| transition: background-color 160ms ease, opacity 160ms ease; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .network-button:hover:not(:disabled) { | ||
| background: #f5f5f5; | ||
| } | ||
|
|
||
| .network-button:disabled { | ||
| cursor: not-allowed; | ||
| opacity: 0.62; | ||
| } | ||
|
|
||
| .network-actions { | ||
| justify-content: flex-end; | ||
| padding-top: 4px; | ||
| } | ||
|
|
||
| .completion-panel { | ||
| align-items: center; | ||
| background: var(--builder-orange-gradient); | ||
| border-top: 1px solid var(--builder-orange-gradient-border); | ||
| display: flex; | ||
| gap: 18px; | ||
| justify-content: space-between; | ||
| overflow: hidden; | ||
| padding: 18px; | ||
| position: relative; | ||
| } | ||
|
|
||
| .completion-panel::before { | ||
| background: | ||
| radial-gradient(circle at 10% 0%, rgba(255, 255, 255, 0.72), transparent 30%), | ||
| radial-gradient(circle at 88% 100%, rgba(238, 133, 33, 0.12), transparent 32%); | ||
| content: ''; | ||
| inset: 0; | ||
| pointer-events: none; | ||
| position: absolute; | ||
| } | ||
|
|
||
| .completion-panel > * { | ||
| position: relative; | ||
| z-index: 1; | ||
| } | ||
|
|
||
| .completion-panel p { | ||
| color: var(--journey-black); | ||
| font-family: var(--font-display); | ||
| font-size: 18px; | ||
| font-weight: 500; | ||
| line-height: 24px; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .completion-panel span { | ||
| color: #737373; | ||
| display: block; | ||
| font-family: var(--font-body); | ||
| font-size: 13px; | ||
| line-height: 20px; | ||
| margin-top: 2px; | ||
| } | ||
|
|
||
| .landing-button.check-gradient-button { | ||
| background: var(--builder-orange-gradient); | ||
| border-color: var(--builder-orange-gradient-border); | ||
| box-shadow: 0 8px 17px rgba(238, 133, 33, 0.12); | ||
| color: var(--builder-orange-gradient-ink); | ||
| } | ||
|
|
||
| .landing-button.check-gradient-button:hover:not(:disabled) { | ||
| background: var(--builder-orange-gradient-hover); | ||
| color: var(--builder-orange-gradient-ink); | ||
| } | ||
|
|
||
| .landing-button { | ||
| align-items: center; | ||
| border-radius: 20px; | ||
| display: inline-flex; | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| font-weight: 500; | ||
| gap: 8px; | ||
| height: 40px; | ||
| justify-content: center; | ||
| letter-spacing: 0.28px; | ||
| line-height: 21px; | ||
| padding: 0 16px; | ||
| transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .landing-button:disabled { | ||
| cursor: not-allowed; | ||
| opacity: 0.62; | ||
| } | ||
|
|
||
| .landing-button-primary { | ||
| background: var(--journey-black); | ||
| color: #fff; | ||
| } | ||
|
|
||
| .landing-button-primary:hover:not(:disabled) { | ||
| background: #2a292c; | ||
| } | ||
|
|
||
| .landing-button-secondary { | ||
| border: 1px solid var(--journey-black); | ||
| color: var(--journey-black); | ||
| } | ||
|
|
||
| .landing-button-secondary:hover:not(:disabled) { | ||
| background: #f5f5f5; | ||
| } | ||
|
|
||
| .unlock-section { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 16px; | ||
| padding-top: 16px; | ||
| } | ||
|
|
||
| .section-label { | ||
| align-items: center; | ||
| display: flex; | ||
| gap: 12px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .section-label p { | ||
| color: #ababab; | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| letter-spacing: 1.54px !important; | ||
| line-height: 17px; | ||
| margin: 0; | ||
| text-transform: uppercase; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .section-label span { | ||
| background: #e6e6e6; | ||
| flex: 1 1 auto; | ||
| height: 1px; | ||
| } | ||
|
|
||
| .unlock-grid { | ||
| display: grid; | ||
| gap: 20px; | ||
| grid-template-columns: repeat(3, minmax(0, 1fr)); | ||
| } | ||
|
|
||
| @media (max-width: 1180px) { | ||
| .journey-page { | ||
| max-width: 920px; | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 900px) { | ||
| .task-panel, | ||
| .compact-panel { | ||
| grid-template-columns: 1fr; | ||
| padding-left: 18px; | ||
| } | ||
|
|
||
| .panel-actions { | ||
| justify-content: flex-start; | ||
| } | ||
|
|
||
| .completion-panel { | ||
| align-items: flex-start; | ||
| flex-direction: column; | ||
| } | ||
|
|
||
| .unlock-grid { | ||
| grid-template-columns: 1fr; | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 640px) { | ||
| .journey-page { | ||
| gap: 20px; | ||
| padding: 12px 0 56px; | ||
| } | ||
|
|
||
| .task-panel, | ||
| .compact-panel { | ||
| padding: 14px; | ||
| } | ||
|
|
||
| .section-label { | ||
| padding: 0 2px; | ||
| } | ||
|
|
||
| .completion-panel { | ||
| padding: 16px 14px; | ||
| } | ||
| } | ||
| </style> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Move this route styling to Tailwind utilities.
The new route relies on a large custom stylesheet with hardcoded spacing, colors, and responsive rules instead of Tailwind utility classes.
As per coding guidelines, "frontend/**/*.svelte: Use Tailwind CSS utility classes for styling instead of custom CSS."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/BuilderJourney.svelte` around lines 733 - 1131, The
BuilderJourney route still uses a large custom <style> block instead of Tailwind
utilities. Move the styling for the .journey-page layout and its sections like
.steps-card, .task-panel, .network-item, .completion-panel, and .unlock-grid
into the Svelte markup using Tailwind class names, including responsive behavior
now handled by the media queries. Keep the component structure intact while
replacing the hardcoded spacing, colors, borders, and hover states with
equivalent utility classes.
Source: Coding guidelines
| const textarea = document.createElement('textarea'); | ||
| textarea.value = text; | ||
| textarea.setAttribute('readonly', ''); | ||
| textarea.style.position = 'absolute'; | ||
| textarea.style.left = '-9999px'; | ||
| document.body.appendChild(textarea); | ||
| textarea.select(); | ||
| document.execCommand('copy'); | ||
| document.body.removeChild(textarea); | ||
| showSuccess('Post text copied.'); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🌐 Web query:
MDN document.execCommand copy return value false unsuccessful
💡 Result:
When document.execCommand('copy') returns false, it indicates that the copy command was either unsupported by the browser or disabled [1][2]. The most common reason for this return value is that the command was not triggered by a user-initiated interaction (such as a click or keypress) [1][3]. For security and privacy reasons, browsers generally restrict clipboard access, requiring that such commands occur within the context of a trusted user event [3]. If you attempt to call this method from the browser's developer console or from a script that is not responding to a direct user action, it will typically return false [3]. Additionally, the command may return false or fail if: - There is no active text selection to copy [4][5][6]. - The browser implementation does not support the 'copy' command [1][7]. - The script lacks the necessary permissions [4]. Note that document.execCommand is considered a deprecated API [8]. For modern web applications, the Clipboard API (navigator.clipboard.writeText) is the recommended alternative, as it provides a more robust and asynchronous way to interact with the system clipboard. Because document.execCommand only returns true when invoked successfully during a user interaction, you cannot use it to reliably check for browser support before attempting the operation [1][2]. Instead, you should rely on established feature detection or the more modern Clipboard API [5][8].
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
- 2: https://github.com/mdn/content/blob/main/files/en-us/web/api/document/execcommand/index.md
- 3: https://stackoverflow.com/questions/33321095/cannot-use-document-execcommandcopy-from-developer-console
- 4: https://developer.mozilla.org/en-US/docs/Web/API/Document/queryCommandEnabled
- 5: https://developer.chrome.com/blog/cut-and-copy-commands
- 6: https://stackoverflow.com/questions/35719922/find-out-why-querycommandenabled-returns-false
- 7: https://w3c.github.io/editing/docs/execCommand/
- 8: https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file and surrounding clipboard logic.
git ls-files 'frontend/src/routes/CommunityJourney.svelte'
wc -l frontend/src/routes/CommunityJourney.svelte
sed -n '140,220p' frontend/src/routes/CommunityJourney.svelte
# Find any other clipboard/toast handling in the file.
rg -n "execCommand|clipboard|showSuccess|showError" frontend/src/routes/CommunityJourney.svelteRepository: genlayer-foundation/points
Length of output: 3241
Handle execCommand('copy') failures in the fallback
frontend/src/routes/CommunityJourney.svelte:174-183 — document.execCommand('copy') can return false, so the fallback still shows “Post text copied.” even when nothing was copied. Show success only when it returns true; otherwise surface an error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/CommunityJourney.svelte` around lines 174 - 183, The copy
fallback in CommunityJourney.svelte currently assumes
document.execCommand('copy') always succeeds, so update the logic around the
textarea-based copy flow to check the return value from execCommand before
calling showSuccess. If it returns true, keep the success message; if it returns
false, surface an error instead, using the same copy helper/handler that
performs the fallback.
| <style> | ||
| .journey-page { | ||
| --role-accent: #8d81e1; | ||
| --role-accent-hover: #7669d4; | ||
| --journey-active-bg: #f6f3ff; | ||
| --journey-black: #131214; | ||
| --journey-border: #ededed; | ||
| --journey-muted: #909090; | ||
| --journey-hero-bg: linear-gradient(163deg, #fff 40%, #f7f5ff 96%); | ||
| --journey-hero-border: #e6e0ff; | ||
| --journey-hero-glow: rgba(141, 129, 225, 0.18); | ||
| --journey-points-bg: #f1efff; | ||
| --journey-complete-gradient: linear-gradient(135deg, #fff 0%, #f7f5ff 52%, #e8e3ff 100%); | ||
| --journey-complete-border: #ddd6ff; | ||
| --journey-complete-color: #7669d4; | ||
| --journey-complete-shadow: rgba(141, 129, 225, 0.14); | ||
| color: #000; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| margin: 0 auto; | ||
| max-width: 940px; | ||
| padding: 20px 12px 80px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .journey-page :global(*) { | ||
| letter-spacing: 0; | ||
| } | ||
|
|
||
| .steps-card { | ||
| background: #fff; | ||
| border: 1px solid var(--journey-border); | ||
| border-radius: 14px; | ||
| overflow: hidden; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .step-block { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .task-panel { | ||
| background: linear-gradient(90deg, var(--journey-active-bg) 0%, #fff 82%); | ||
| border-top: 1px solid #f0f0f0; | ||
| display: grid; | ||
| gap: 16px; | ||
| grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); | ||
| padding: 16px 18px 18px 58px; | ||
| } | ||
|
|
||
| .task-panel-copy p, | ||
| .x-post-copy p { | ||
| color: #737373; | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| line-height: 22px; | ||
| margin: 0; | ||
| max-width: 430px; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn), | ||
| .social-link-frame :global(.social-connected-row) { | ||
| border-radius: 20px; | ||
| font-family: var(--font-body); | ||
| min-height: 40px; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn) { | ||
| background: var(--journey-black) !important; | ||
| } | ||
|
|
||
| .social-link-frame :global(.social-connect-btn:hover:not(:disabled)) { | ||
| background: #2a292c !important; | ||
| } | ||
|
|
||
| .task-card-frame, | ||
| .social-link-frame { | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .x-post-panel { | ||
| background: linear-gradient(90deg, var(--journey-active-bg) 0%, #fff 82%); | ||
| border-top: 1px solid #f0f0f0; | ||
| display: grid; | ||
| gap: 18px; | ||
| grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); | ||
| padding: 16px 18px 18px 58px; | ||
| } | ||
|
|
||
| .x-post-copy { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 14px; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .share-box { | ||
| background: #fff; | ||
| border: 1px solid #e8e4fb; | ||
| border-radius: 10px; | ||
| display: grid; | ||
| gap: 12px; | ||
| grid-template-columns: minmax(0, 1fr) auto; | ||
| padding: 12px; | ||
| } | ||
|
|
||
| .share-box span { | ||
| color: var(--journey-black); | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| line-height: 22px; | ||
| overflow-wrap: anywhere; | ||
| } | ||
|
|
||
| .share-box button { | ||
| color: var(--role-accent); | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| letter-spacing: 1px; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .share-box button:disabled { | ||
| opacity: 0.45; | ||
| } | ||
|
|
||
| .x-post-actions { | ||
| display: flex; | ||
| } | ||
|
|
||
| .verify-card { | ||
| background: #fff; | ||
| border: 1px solid #e8e4fb; | ||
| border-radius: 10px; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 10px; | ||
| padding: 12px; | ||
| } | ||
|
|
||
| .verify-card label { | ||
| color: #737373; | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| letter-spacing: 1px; | ||
| line-height: 17px; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .verify-card input { | ||
| border: 1px solid #e6e6e6; | ||
| border-radius: 8px; | ||
| color: var(--journey-black); | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| height: 40px; | ||
| outline: none; | ||
| padding: 0 12px; | ||
| transition: border-color 160ms ease, box-shadow 160ms ease; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .verify-card input:focus { | ||
| border-color: var(--role-accent); | ||
| box-shadow: 0 0 0 3px rgba(141, 129, 225, 0.14); | ||
| } | ||
|
|
||
| .completion-panel { | ||
| background: var(--journey-complete-gradient); | ||
| border-top: 1px solid var(--journey-complete-border); | ||
| padding: 18px; | ||
| } | ||
|
|
||
| .completion-panel p { | ||
| color: var(--journey-black); | ||
| font-family: var(--font-display); | ||
| font-size: 18px; | ||
| font-weight: 500; | ||
| line-height: 24px; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .completion-panel span { | ||
| color: #737373; | ||
| display: block; | ||
| font-family: var(--font-body); | ||
| font-size: 13px; | ||
| line-height: 20px; | ||
| margin-top: 2px; | ||
| } | ||
|
|
||
| .landing-button { | ||
| align-items: center; | ||
| border-radius: 20px; | ||
| display: inline-flex; | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| font-weight: 500; | ||
| gap: 8px; | ||
| height: 40px; | ||
| justify-content: center; | ||
| letter-spacing: 0.28px !important; | ||
| line-height: 21px; | ||
| padding: 0 16px; | ||
| transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .landing-button:disabled { | ||
| cursor: default; | ||
| opacity: 0.62; | ||
| } | ||
|
|
||
| .landing-button-primary { | ||
| background: var(--journey-black); | ||
| color: #fff; | ||
| } | ||
|
|
||
| .landing-button-primary:hover:not(:disabled) { | ||
| background: #2a292c; | ||
| } | ||
|
|
||
| .landing-button-secondary { | ||
| border: 1px solid var(--journey-black); | ||
| color: var(--journey-black); | ||
| } | ||
|
|
||
| .landing-button-secondary:hover:not(:disabled) { | ||
| background: #f5f5f5; | ||
| } | ||
|
|
||
| .unlock-section { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 16px; | ||
| padding-top: 16px; | ||
| } | ||
|
|
||
| .section-label { | ||
| align-items: center; | ||
| display: flex; | ||
| gap: 12px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .section-label p { | ||
| color: #ababab; | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| letter-spacing: 1.54px !important; | ||
| line-height: 17px; | ||
| margin: 0; | ||
| text-transform: uppercase; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .section-label span { | ||
| background: #e6e6e6; | ||
| flex: 1 1 auto; | ||
| height: 1px; | ||
| } | ||
|
|
||
| .unlock-grid { | ||
| display: grid; | ||
| gap: 20px; | ||
| grid-template-columns: repeat(3, minmax(0, 1fr)); | ||
| } | ||
|
|
||
| @media (max-width: 1180px) { | ||
| .journey-page { | ||
| max-width: 920px; | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 900px) { | ||
| .task-panel, | ||
| .x-post-panel { | ||
| grid-template-columns: 1fr; | ||
| padding-left: 18px; | ||
| } | ||
|
|
||
| .unlock-grid { | ||
| grid-template-columns: 1fr; | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 640px) { | ||
| .journey-page { | ||
| gap: 20px; | ||
| padding: 12px 0 56px; | ||
| } | ||
|
|
||
| .task-panel, | ||
| .x-post-panel { | ||
| padding: 14px; | ||
| } | ||
|
|
||
| .share-box { | ||
| grid-template-columns: 1fr; | ||
| } | ||
|
|
||
| .section-label { | ||
| padding: 0 2px; | ||
| } | ||
| } | ||
| </style> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Move this route styling to Tailwind utilities.
The route introduces a large component-scoped stylesheet instead of Tailwind utility classes.
As per coding guidelines, "frontend/**/*.svelte: Use Tailwind CSS utility classes for styling instead of custom CSS."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/CommunityJourney.svelte` around lines 478 - 784, The
CommunityJourney route still defines a large component-scoped <style> block,
which violates the Tailwind-only styling guideline. Move the styles from the
JourneyPage layout and its sections (for example journey-page, task-panel,
x-post-panel, share-box, verify-card, completion-panel, landing-button,
unlock-section, and unlock-grid) into Tailwind utility classes directly in the
Svelte markup, keeping the same responsive behavior and state styling via
utility variants instead of custom CSS.
Source: Coding guidelines
| try { | ||
| await journeyAPI.startValidatorJourney(); | ||
| // Store success message and redirect to profile | ||
| await userStore.loadUser?.(); | ||
| sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!'); | ||
| push(`/participant/${$authState.address}`); | ||
| replace('/validators'); | ||
| } catch (err) { | ||
| error = err.response?.data?.error || 'Failed to join waitlist'; | ||
| isJoiningWaitlist = false; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Do not let the success-message storage write fail the completed join.
If sessionStorage.setItem throws after startValidatorJourney() succeeds, the catch path reports “Failed to join waitlist” even though the backend mutation already happened.
Proposed fix
await journeyAPI.startValidatorJourney();
await userStore.loadUser?.();
- sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!');
+ try {
+ sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!');
+ } catch {
+ // Non-critical: redirect still reflects the server-side waitlist state.
+ }
replace('/validators');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await journeyAPI.startValidatorJourney(); | |
| // Store success message and redirect to profile | |
| await userStore.loadUser?.(); | |
| sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!'); | |
| push(`/participant/${$authState.address}`); | |
| replace('/validators'); | |
| } catch (err) { | |
| error = err.response?.data?.error || 'Failed to join waitlist'; | |
| isJoiningWaitlist = false; | |
| try { | |
| await journeyAPI.startValidatorJourney(); | |
| await userStore.loadUser?.(); | |
| try { | |
| sessionStorage.setItem('journeySuccess', 'Successfully joined Validator Waitlist!'); | |
| } catch { | |
| // Non-critical: redirect still reflects the server-side waitlist state. | |
| } | |
| replace('/validators'); | |
| } catch (err) { | |
| error = err.response?.data?.error || 'Failed to join waitlist'; | |
| isJoiningWaitlist = false; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/ValidatorWaitlist.svelte` around lines 85 - 92, The join
flow in ValidatorWaitlist.svelte treats a failure in sessionStorage.setItem as
if startValidatorJourney failed, which incorrectly shows “Failed to join
waitlist” after the backend call already succeeded. Update the success path
around startValidatorJourney, userStore.loadUser, and replace so the persistence
write for journeySuccess is isolated from the mutation error handling, and
handle any storage exception without entering the main catch or setting the
join-failed error state.
| } catch (err) { | ||
| error = err.response?.data?.error || 'Failed to join waitlist'; | ||
| isJoiningWaitlist = false; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Show a toast when waitlist submission fails.
The route sets inline error state, but the app guideline requires API errors in routes to surface through the toast notification system too.
As per coding guidelines, "frontend/**/{components,routes}/**/*.{ts,tsx,js,svelte}: Always handle API errors with try-catch blocks and provide user-facing error messages via toast notifications."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/ValidatorWaitlist.svelte` around lines 90 - 92, The
waitlist submission failure in ValidatorWaitlist.svelte only updates inline
error state and does not surface the API error through the toast system. In the
existing try-catch around the waitlist join flow, add a toast notification in
the catch block alongside the current error assignment and loading reset so
users get a visible failure message. Use the route’s waitlist submission logic
and its existing error handling path to keep the user-facing feedback consistent
with the app guideline.
Source: Coding guidelines
| <style> | ||
| .journey-page { | ||
| --role-accent: #387de8; | ||
| --role-accent-hover: #2f6fd4; | ||
| --journey-active-bg: #f0f6ff; | ||
| --journey-black: #131214; | ||
| --journey-border: #ededed; | ||
| --journey-muted: #909090; | ||
| --journey-hero-bg: linear-gradient(163deg, #fff 40%, #f1f7ff 96%); | ||
| --journey-hero-border: #dceafe; | ||
| --journey-hero-glow: rgba(56, 125, 232, 0.17); | ||
| --journey-points-bg: #edf5ff; | ||
| --journey-complete-gradient: linear-gradient(135deg, #5c9af1 0%, #387de8 100%); | ||
| --journey-complete-shadow: rgba(56, 125, 232, 0.19); | ||
| color: #000; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 24px; | ||
| margin: 0 auto; | ||
| max-width: 940px; | ||
| padding: 20px 12px 80px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .journey-page :global(*) { | ||
| letter-spacing: 0; | ||
| } | ||
|
|
||
| .steps-card { | ||
| background: #fff; | ||
| border: 1px solid var(--journey-border); | ||
| border-radius: 14px; | ||
| overflow: hidden; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .step-block { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .step-check-panel { | ||
| background: linear-gradient(90deg, var(--journey-active-bg) 0%, #fff 82%); | ||
| border-top: 1px solid #f0f0f0; | ||
| padding: 14px 18px 18px 58px; | ||
| } | ||
|
|
||
| .form-confirmation { | ||
| align-items: flex-start; | ||
| background: #fff; | ||
| border: 1px solid #dceafe; | ||
| border-radius: 12px; | ||
| color: var(--journey-black); | ||
| cursor: pointer; | ||
| display: flex; | ||
| font-family: var(--font-body); | ||
| gap: 11px; | ||
| max-width: 520px; | ||
| padding: 12px; | ||
| } | ||
|
|
||
| .form-confirmation input { | ||
| accent-color: var(--role-accent); | ||
| flex: 0 0 auto; | ||
| height: 16px; | ||
| margin: 2px 0 0; | ||
| width: 16px; | ||
| } | ||
|
|
||
| .form-confirmation span { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 2px; | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .form-confirmation strong { | ||
| color: var(--journey-black); | ||
| font-size: 13.5px; | ||
| font-weight: 600; | ||
| line-height: 20px; | ||
| } | ||
|
|
||
| .form-confirmation small { | ||
| color: #737373; | ||
| font-size: 12.5px; | ||
| line-height: 19px; | ||
| } | ||
|
|
||
| .graduation-section { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 20px; | ||
| padding-top: 4px; | ||
| } | ||
|
|
||
| .section-label { | ||
| align-items: center; | ||
| display: flex; | ||
| gap: 12px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .section-label p { | ||
| color: #ababab; | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| line-height: 17px; | ||
| margin: 0; | ||
| text-transform: uppercase; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .section-label span { | ||
| background: #e6e6e6; | ||
| flex: 1 1 auto; | ||
| height: 1px; | ||
| } | ||
|
|
||
| .graduation-grid { | ||
| display: grid; | ||
| gap: 18px; | ||
| grid-template-columns: repeat(3, minmax(0, 1fr)); | ||
| } | ||
|
|
||
| .graduation-card { | ||
| background: #fff; | ||
| border: 1px solid var(--journey-border); | ||
| border-radius: 16px; | ||
| min-height: 236px; | ||
| padding: 24px 26px 26px; | ||
| position: relative; | ||
| } | ||
|
|
||
| .lock-badge { | ||
| align-items: center; | ||
| background: #fff; | ||
| border: 1px solid #f0f0f0; | ||
| border-radius: 6px; | ||
| color: #b7b7b7; | ||
| display: inline-flex; | ||
| font-family: var(--font-mono); | ||
| font-size: 10px; | ||
| height: 20px; | ||
| justify-content: center; | ||
| line-height: 16px; | ||
| min-width: 68px; | ||
| position: absolute; | ||
| right: 12px; | ||
| text-transform: uppercase; | ||
| top: 14px; | ||
| } | ||
|
|
||
| .outline-hex { | ||
| height: 48px; | ||
| margin-bottom: 18px; | ||
| width: 48px; | ||
| } | ||
|
|
||
| .outline-hex img { | ||
| height: 100%; | ||
| opacity: 0.9; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .graduation-card h2, | ||
| .contact-card h2 { | ||
| color: var(--journey-black); | ||
| font-family: var(--font-display); | ||
| font-size: 22px; | ||
| font-weight: 500; | ||
| line-height: 28px; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .graduation-card p { | ||
| color: #737373; | ||
| font-family: var(--font-body); | ||
| font-size: 15px; | ||
| line-height: 24px; | ||
| margin: 10px 0 0; | ||
| } | ||
|
|
||
| .graduation-card a { | ||
| color: #5e7cf6; | ||
| display: inline-flex; | ||
| font-family: var(--font-body); | ||
| font-size: 13px; | ||
| font-weight: 600; | ||
| line-height: 20px; | ||
| margin-top: 14px; | ||
| text-decoration: none; | ||
| } | ||
|
|
||
| .graduation-card a:hover { | ||
| color: var(--role-accent-hover); | ||
| } | ||
|
|
||
| .contact-card { | ||
| align-items: center; | ||
| background: #fff; | ||
| border: 1px solid var(--journey-border); | ||
| border-radius: 18px; | ||
| display: grid; | ||
| gap: 18px; | ||
| grid-template-columns: 40px minmax(0, 1fr) auto; | ||
| padding: 28px 30px; | ||
| } | ||
|
|
||
| .contact-icon { | ||
| height: 40px; | ||
| position: relative; | ||
| width: 40px; | ||
| } | ||
|
|
||
| .contact-icon img:first-child { | ||
| height: 100%; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .contact-icon img:last-child { | ||
| height: 18px; | ||
| left: 50%; | ||
| position: absolute; | ||
| top: 50%; | ||
| transform: translate(-50%, -50%); | ||
| width: 18px; | ||
| } | ||
|
|
||
| .contact-copy { | ||
| min-width: 0; | ||
| } | ||
|
|
||
| .role-eyebrow { | ||
| color: var(--role-accent); | ||
| font-family: var(--font-mono); | ||
| font-size: 11px; | ||
| line-height: 17px; | ||
| margin: 0 0 8px; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .contact-copy p:not(.role-eyebrow) { | ||
| color: #3f3f3f; | ||
| font-family: var(--font-body); | ||
| font-size: 14.5px; | ||
| line-height: 23px; | ||
| margin: 8px 0 0; | ||
| max-width: 620px; | ||
| } | ||
|
|
||
| .contact-copy strong { | ||
| font-weight: 700; | ||
| } | ||
|
|
||
| .landing-button { | ||
| align-items: center; | ||
| border-radius: 20px; | ||
| display: inline-flex; | ||
| font-family: var(--font-body); | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| height: 40px; | ||
| justify-content: center; | ||
| line-height: 20px; | ||
| padding: 0 20px; | ||
| text-decoration: none; | ||
| transition: background-color 160ms ease, opacity 160ms ease; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .landing-button-accent { | ||
| background: var(--role-accent); | ||
| color: #fff; | ||
| } | ||
|
|
||
| .landing-button-accent:hover { | ||
| background: var(--role-accent-hover); | ||
| } | ||
|
|
||
| @media (max-width: 900px) { | ||
| .graduation-grid { | ||
| grid-template-columns: 1fr; | ||
| } | ||
|
|
||
| .contact-card { | ||
| align-items: flex-start; | ||
| grid-template-columns: 40px minmax(0, 1fr); | ||
| } | ||
|
|
||
| .contact-card .landing-button { | ||
| grid-column: 2; | ||
| justify-self: flex-start; | ||
| } | ||
| } | ||
|
|
||
| @media (max-width: 640px) { | ||
| .journey-page { | ||
| gap: 20px; | ||
| padding: 12px 0 56px; | ||
| } | ||
|
|
||
| .step-check-panel { | ||
| padding: 14px; | ||
| } | ||
|
|
||
| .section-label { | ||
| padding: 0 2px; | ||
| } | ||
|
|
||
| .section-label p { | ||
| white-space: normal; | ||
| } | ||
|
|
||
| .graduation-card { | ||
| min-height: 0; | ||
| padding: 22px; | ||
| } | ||
|
|
||
| .contact-card { | ||
| grid-template-columns: 1fr; | ||
| padding: 22px; | ||
| } | ||
|
|
||
| .contact-card .landing-button { | ||
| grid-column: 1; | ||
| width: 100%; | ||
| } | ||
| } | ||
| </style> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Move this route styling to Tailwind utilities.
The new waitlist layout is implemented with a large custom <style> block instead of Tailwind utility classes.
As per coding guidelines, "frontend/**/*.svelte: Use Tailwind CSS utility classes for styling instead of custom CSS."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/routes/ValidatorWaitlist.svelte` around lines 210 - 538, The
ValidatorWaitlist.svelte route still uses a large custom style block instead of
Tailwind utilities, which violates the frontend styling guideline. Replace the
CSS for the journey layout by moving the styling into the Svelte markup using
Tailwind classes on the relevant elements such as journey-page, steps-card,
step-check-panel, graduation-grid, graduation-card, contact-card, and
landing-button. Preserve the current responsive behavior and visual hierarchy by
translating the media-query-driven layout into Tailwind responsive variants
rather than keeping custom CSS.
Source: Coding guidelines
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/users/views.py (1)
769-783: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle malformed Sorsa tweet payloads before indexing.
tweet['username']andtweet['full_text']can raise if Sorsa returns an incomplete payload, turning a verification outage into a 500. Treat missing fields likeverification_unavailable.Proposed fix
+ if not isinstance(tweet, dict): + return Response( + {'error': 'verification_unavailable', 'message': 'Could not verify your post right now. Please try again shortly.'}, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + # Authoritative author check (Sorsa) against the linked handle. Fail # closed: an absent/empty author must not pass the check. - author = (tweet['username'] or '').lower() + author = (tweet.get('username') or '').lower() if not author: return Response( {'error': 'verification_unavailable', 'message': 'Could not verify your post right now. Please try again shortly.'}, status=status.HTTP_503_SERVICE_UNAVAILABLE, ) @@ - ok, error_code = cj.post_matches(tweet['full_text'], user) + full_text = tweet.get('full_text') or '' + if not full_text: + return Response( + {'error': 'verification_unavailable', 'message': 'Could not verify your post right now. Please try again shortly.'}, + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) + + ok, error_code = cj.post_matches(full_text, user)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/users/views.py` around lines 769 - 783, The Sorsa verification path in the tweet-checking logic can crash on incomplete payloads because it indexes tweet['username'] and tweet['full_text'] directly. Update the verification flow around the author check and cj.post_matches call to safely read these fields and treat any missing or empty username/full_text as verification_unavailable instead of letting a KeyError bubble up. Keep the fail-closed behavior in the same verification branch, using the existing linked_handle check and Response error handling to return the unavailable status consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@backend/users/views.py`:
- Around line 769-783: The Sorsa verification path in the tweet-checking logic
can crash on incomplete payloads because it indexes tweet['username'] and
tweet['full_text'] directly. Update the verification flow around the author
check and cj.post_matches call to safely read these fields and treat any missing
or empty username/full_text as verification_unavailable instead of letting a
KeyError bubble up. Keep the fail-closed behavior in the same verification
branch, using the existing linked_handle check and Response error handling to
return the unavailable status consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 85181076-9e6c-45da-939e-86aa2a0ee876
📒 Files selected for processing (3)
backend/users/views.pyfrontend/src/components/funnel/CommunityLanding.sveltefrontend/src/components/funnel/ValidatorLanding.svelte
Reworks onboarding for Builders, Validators, and Community into role-specific funnels: each landing page adapts to signed-out, signed-in-without-role, and earned states, with dedicated step-by-step journey routes. Journeys now grant the role itself rather than points, so the previous farmable point grants are removed and points come only from verifiable tasks such as starring the boilerplate repo or posting on X. The Builder journey is connect GitHub plus star the boilerplate repo; the Community journey is five verified steps (link X, link Discord, follow, join, and a verified X post checked against the linked handle) that grant the Creator role; the Validator journey stays a single waitlist form and keeps the graduation reward. Linking GitHub now records a contribution like X and Discord, and Community membership is created only by completing the journey, so the previous automatic creation from contributions, POAP claims, and Discord XP is removed. Sidebar subsections stay locked until the relevant journey is complete, the profile shows in-progress roles with an owner-only badge, and the What's New dialog appears only to returning users.
Summary by CodeRabbit
New Features
Bug Fixes
Tests