Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 28 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,40 @@
Canonical reference: https://github.com/basedosdados/backend/wiki/Boas-Pr%C3%A1ticas#segue-o-fluxo

### The environment branches — never commit or push directly
- `main` — production (basedosdados.org).
- `main` — production (basedosdados.org). Source of truth: resets flow *from* `main`.
- `staging` — pre-production / QA.
- `development` — integration / testing. First target for new work.
- `development` — integration / testing.

These three are **parallel, independently maintained** branches, not a linear chain.
Their histories have diverged, so you never merge one environment branch into another to
move a feature — that would drag the whole environment forward. Each feature is promoted
into each environment **selectively**, via its own PR carrying only that feature.
### How work flows
Every feature starts from `main` and is promoted back into the other environments using
**one branch and three PRs** — the same head branch is PR'd into `development`, `staging`, and
`main`. Merging the same branch into each target puts the *same commit objects* into all
three, so the feature's own commits share SHAs everywhere. Only the merge commits differ,
which is expected and fine.

Note: the integration branch here is named `development` (full word), not `dev`.
The environments still drift apart as those merge commits accumulate at different times, so
the team **resets `staging` and `development` back to `main` roughly every two weeks**. Because
resets flow *from* `main`, a change must reach `main` to survive — anything living only on
`staging` or `development` is discarded at the next reset.

### Feature workflow — promote the feature, not the environment
1. Cut your feature branch off `development` (the branch you integrate and test in first).
### Feature workflow — one branch, three PRs
1. Cut your feature branch off `main` — never off `staging` or `development`.
Name it by intent: `feat/…`, `fix/…`, `chore/…`, `docs/…`, `refactor/…`.
Keep one logical change per branch, with tidy commits — you will cherry-pick them.
2. Open a PR from that branch into `development`.
3. To promote the same feature to `staging`, cut a new branch off `staging` and
cherry-pick only this feature's commit(s) onto it, then open a PR into `staging`.
4. To promote to `main`, repeat: cut a branch off `main`, cherry-pick the same commit(s),
open a PR into `main`.
5. Result: one clean PR per environment, each carrying only this feature.
One logical change per branch.
2. From that **same branch**, open three PRs: one into `development`, one into `staging`, one
into `main`. Do not cut a separate branch per target, and do not cherry-pick.
3. Merge with a **merge commit or fast-forward — never squash**. A squash mints a new,
unrelated commit on each branch and breaks the shared history the resets rely on.
4. Timing: a `main`-based branch merges cleanly into `development`/`staging` when those are
aligned with `main` — in practice, shortly after a reset. The longer since the last
reset, the more of `main`'s accumulated commits the PR will drag along. If a target has
drifted far, wait for the reset rather than forcing a noisy merge.

### Rules for agents working in this repo
- Never commit or push to `main`, `staging`, or `development` directly.
- Move a feature between environments by cherry-picking it onto a branch cut off the
target — never by merging `development → staging` or `staging → main`.
- Each promotion branch is cut off its own target, so the PR diff is only this feature.
- One logical change per branch; one PR at a time per target; keep commits clean for cherry-picking.
- Never commit or push to `main`, `staging`, or `development` directly — always a feature branch + PR.
- Always cut features off `main`, never off `staging` or `development`.
- Use **one branch for all three PRs**. Never a branch-per-target, never cherry-pick.
- **Never squash-merge.** Merge commit or fast-forward only.
- Never merge one environment branch into another to promote a feature.
- Before committing, verify you are on a feature branch: `git branch --show-current`.
- Do not push without explicit permission.
4 changes: 2 additions & 2 deletions next/components/molecules/ImgCrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function CropImage ({
onClose,
src,
id,
username,
email,
}) {
const { t } = useTranslation('user');
const imgRef = useRef(null)
Expand Down Expand Up @@ -147,7 +147,7 @@ export default function CropImage ({
})
})

const filePic = new File([picture], `${username}.jpeg`, {type: "image/jpeg"})
const filePic = new File([picture], `${email || id}.jpeg`, {type: "image/jpeg"})

const reg = new RegExp("(?<=:).*")
const [ uid ] = reg.exec(id)
Expand Down
16 changes: 9 additions & 7 deletions next/components/molecules/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { ControlledInputSimple } from "../atoms/ControlledInput";
import Link from "../atoms/Link";
import Button from "../atoms/Button";
import HelpWidget from "../atoms/HelpWidget";
import { triggerGAEvent, triggerGAEventWithData, hasBDProSubscription, hasChatbotSubscription, trackNavigateToChatbotLp, clearClientSession, getDiscordUrl } from "../../utils";
import { triggerGAEvent, triggerGAEventWithData, hasBDProSubscription, hasChatbotSubscription, trackNavigateToChatbotLp, clearClientSession, getDiscordUrl, getUserDisplayName, getUserPageHref, UserPagePath } from "../../utils";

import LabelText from "../atoms/Text/LabelText";
import BodyText from "../atoms/Text/BodyText";
Expand Down Expand Up @@ -346,7 +346,7 @@ function MenuDrawerUser({ userData, isOpen, onClose, isUserPro, haveInterprisePl
src={userData?.picture ? userData?.picture : "https://storage.googleapis.com/basedosdados-website/equipe/sem_foto.png"}
/>
</Box>
<LabelText typography="x-small">{userData?.username || ""}</LabelText>
<LabelText typography="x-small">{getUserDisplayName(userData)}</LabelText>
<LabelText
typography="x-small"
color="#71757A"
Expand Down Expand Up @@ -402,7 +402,7 @@ function MenuDrawerUser({ userData, isOpen, onClose, isUserPro, haveInterprisePl
fontWeight="400"
onClick={() => {
onClose()
router.push({ pathname: `/user/${userData.username}`, query: elm.value })
router.push(getUserPageHref(elm.value))
}}
>
{elm.name}
Expand Down Expand Up @@ -593,7 +593,7 @@ function MenuUser ({ userData, onOpen, onClose, isUserPro }) {
/>
</Box>
<LabelText typography="x-small">
{userData?.username ? userData?.username : ""}
{getUserDisplayName(userData)}
</LabelText>
<LabelText
typography="x-small"
Expand Down Expand Up @@ -630,7 +630,7 @@ function MenuUser ({ userData, onOpen, onClose, isUserPro }) {
gap="8px"
padding="16px"
_hover={{ backgroundColor: "transparent", opacity: "0.7" }}
onClick={() => router.push(`/user/${userData.username}`)}
onClick={() => router.push(UserPagePath)}
>
<SettingsIcon fill="#D0D0D0" width="20px" height="20px"/>
<BodyText typography="small">
Expand All @@ -647,7 +647,7 @@ function MenuUser ({ userData, onOpen, onClose, isUserPro }) {
_hover={{ backgroundColor: "transparent", opacity: "0.7" }}
onClick={async () => {
await clearClientSession()
if(window.location.pathname.includes('/user/')) return window.location.href = "/"
if(window.location.pathname === "/user" || window.location.pathname.includes("/user/")) return window.location.href = "/"
window.location.reload()
}}
>
Expand Down Expand Up @@ -914,6 +914,7 @@ function DesktopLinks({
<HStack spacing="21px" display={{ base: "none", lg: "flex" }}>
{(path === "/search" ||
path === "/dataset/[dataset]" ||
path === "/user" ||
path === "/user/[username]") && (
<Box id="widget_help_and_resources">
<HelpWidget
Expand Down Expand Up @@ -1160,7 +1161,8 @@ export default function MenuNav({ simpleTemplate = false, userTemplate = false }

setUserData({
email: res.email,
username: res.username,
firstName: res.firstName,
lastName: res.lastName,
picture: res.picture || "",
plan: res?.proSubscription
})
Expand Down
89 changes: 89 additions & 0 deletions next/components/molecules/PhoneInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { HStack, Select } from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import {
PhoneCountries,
getPhoneCountry,
handlePhoneInputChange,
} from "../../utils";
import { InputForm } from "./uiUserPage";

const PhoneSelectProps = {
width: "108px",
minWidth: "108px",
height: "40px",
backgroundColor: "#EEEEEE",
border: "2px solid transparent",
borderRadius: "8px",
fontSize: "14px",
lineHeight: "20px",
fontFamily: "Roboto",
fontWeight: "400",
color: "#464A51",
iconColor: "#464A51",
cursor: "pointer",
flexShrink: 0,
_hover: {
border: "2px solid transparent",
backgroundColor: "#DEDFE0",
},
_focus: {
border: "2px solid #0068C5",
backgroundColor: "#FFF",
},
_invalid: {
backgroundColor: "#F6E3E3",
},
}

export default function PhoneInput({
callingCode,
onCallingCodeChange,
value,
onChange,
optional = false,
...props
}) {
const { t } = useTranslation("user")
const { locale } = useRouter()
const showCallingCodeSelect = locale !== "pt"
const country = getPhoneCountry(callingCode)
const placeholder = optional
? `${country.placeholder} ${t("signup.optionalParenthetical")}`
: country.placeholder

const PhoneField = (
<InputForm
id="phone"
name="phone"
type="tel"
autoComplete="tel"
inputMode="numeric"
value={value}
onChange={(e) => onChange(handlePhoneInputChange(value, e.target.value, callingCode))}
placeholder={placeholder}
inputGroupStyle={{ width: "100%" }}
{...props}
/>
)

if (!showCallingCodeSelect) return PhoneField

return (
<HStack spacing="8px" width="100%" align="stretch">
<Select
value={callingCode}
onChange={(e) => onCallingCodeChange(e.target.value)}
aria-label={t("signup.phoneCallingCode")}
{...PhoneSelectProps}
>
{PhoneCountries.map((option) => (
<option key={option.callingCode} value={option.callingCode}>
+{option.callingCode} {option.iso}
</option>
))}
</Select>
{PhoneField}
</HStack>
)
}
Loading