From 1a664d8719ccd2243bed2c6a8c87e083a317b40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jere=20M=C3=A4ennen=C3=A4?= Date: Sun, 11 Jan 2026 14:25:41 +0200 Subject: [PATCH 01/98] Update fi-FI.json --- src/lang/fi-FI.json | 168 ++++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/src/lang/fi-FI.json b/src/lang/fi-FI.json index daaa62f0d..686294701 100644 --- a/src/lang/fi-FI.json +++ b/src/lang/fi-FI.json @@ -37,7 +37,7 @@ "label.compare-dates": "Vertaa päivämääriä", "label.confirm": "Vahvista", "label.confirm-password": "Vahvista salasana", - "label.contains": "Contains", + "label.contains": "Sisältää", "label.content": "Sisältö", "label.continue": "Jatka", "label.conversion": "Konversio", @@ -96,7 +96,7 @@ "label.false": "Epätosi", "label.field": "Kenttä", "label.fields": "Kentät", - "label.filter": "Filter", + "label.filter": "Suodatin", "label.filter-combined": "Yhdistetty", "label.filter-raw": "Käsittelemätön", "label.filters": "Suodattimet", @@ -151,7 +151,7 @@ "label.members": "Jäsenet", "label.min": "Minimi", "label.mobile": "Puhelin", - "label.model": "Model", + "label.model": "Malli", "label.more": "Lisää", "label.my-account": "Oma tili", "label.my-websites": "Omat verkkosivut", @@ -184,9 +184,9 @@ "label.paths": "Polut", "label.pixels": "Pikselit", "label.powered-by": "Voimanlähteenä {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Edellinen", + "label.previous-period": "Edellinen ajanjakso", + "label.previous-year": "Edellinen vuosi", "label.profile": "Profiili", "label.properties": "Ominaisuudet", "label.property": "Ominaisuus", @@ -195,16 +195,16 @@ "label.query-parameters": "Kyselyn parametrit", "label.realtime": "Juuri nyt", "label.referral": "Viittaus", - "label.referrer": "Referrer", + "label.referrer": "Viittaaja", "label.referrers": "Viittaajat", "label.refresh": "Päivitä", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", + "label.regenerate": "Luo uudelleen", + "label.region": "Alue", + "label.regions": "Alueet", "label.remaining": "Jäljellä", - "label.remove": "Remove", - "label.remove-member": "Remove member", - "label.reports": "Reports", + "label.remove": "Poista", + "label.remove-member": "Poista jäsen", + "label.reports": "Raportit", "label.required": "Vaaditaan", "label.reset": "Nollaa", "label.reset-website": "Nollaa tilastot", @@ -212,19 +212,19 @@ "label.retention-description": "Measure your website stickiness by tracking how often users return.", "label.revenue": "Tulot", "label.revenue-description": "Katso tulosi ajan mittaan.", - "label.role": "Role", - "label.run-query": "Run query", + "label.role": "Rooli", + "label.run-query": "Suorita kysely", "label.save": "Tallenna", "label.screens": "Näytöt", - "label.search": "Search", - "label.select": "Select", - "label.select-date": "Select date", + "label.search": "Haku", + "label.select": "Valitse", + "label.select-date": "Valitse päivämäärä", "label.select-filter": "Valitse suodatin", - "label.select-role": "Select role", - "label.select-website": "Select website", + "label.select-role": "Valitse rooli", + "label.select-website": "Valitse verkkosivu", "label.session": "Istunto", "label.session-data": "Istuntotiedot", - "label.sessions": "Sessions", + "label.sessions": "Istunnot", "label.settings": "Asetukset", "label.share": "Jaa", "label.share-url": "Jaa URL", @@ -233,107 +233,107 @@ "label.sources": "Lähteet", "label.start-step": "Aloitusvaihe", "label.steps": "Vaiheet", - "label.sum": "Sum", + "label.sum": "Summa", "label.tablet": "Tabletti", "label.tag": "Tunniste", "label.tags": "Tunnisteet", - "label.team": "Team", - "label.team-id": "Team ID", - "label.team-manager": "Team manager", - "label.team-member": "Team member", - "label.team-name": "Team name", - "label.team-owner": "Team owner", + "label.team": "Tiimi", + "label.team-id": "Tiimin ID", + "label.team-manager": "Tiimin johtaja", + "label.team-member": "Tiimin jäsen", + "label.team-name": "Tiimin nimi", + "label.team-owner": "Tiimin omistaja", "label.team-settings": "Tiimin asetukset", "label.team-view-only": "Team view only", - "label.team-websites": "Team websites", - "label.teams": "Teams", + "label.team-websites": "Tiimin verkkosivut", + "label.teams": "Tiimit", "label.terms": "Ehdot", "label.theme": "Teema", "label.this-month": "Tämä kuukausi", "label.this-week": "Tämä viikko", "label.this-year": "Tämä vuosi", "label.timezone": "Aikavyöhyke", - "label.title": "Title", + "label.title": "Otsikko", "label.today": "Tänään", "label.toggle-charts": "Kytke kaaviot päälle/pois", - "label.total": "Total", - "label.total-records": "Total records", + "label.total": "Yhteensä", + "label.total-records": "Tietueita yhteensä", "label.tracking-code": "Seurantakoodi", - "label.transactions": "Transactions", - "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.transactions": "Transaktiot", + "label.transfer": "Siirrä", + "label.transfer-website": "Siirrä verkkosivu", + "label.true": "Tosi", + "label.type": "Tyyppi", + "label.unique": "Uniikki", "label.unique-visitors": "Yksittäiset kävijät", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Uniikit asiakkaat", "label.unknown": "Tuntematon", - "label.untitled": "Untitled", - "label.update": "Update", - "label.user": "User", + "label.untitled": "Nimetön", + "label.update": "Päivitä", + "label.user": "Käyttäjä", "label.username": "Käyttäjänimi", - "label.users": "Users", + "label.users": "Käyttäjät", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", - "label.value": "Value", - "label.view": "View", + "label.utm-description": "Seuraa kampanjoitasi UTM-parametrien avulla.", + "label.value": "Arvo", + "label.view": "Näytä", "label.view-details": "Katso tiedot", - "label.view-only": "View only", + "label.view-only": "Vain katselu", "label.views": "Näyttökerrat", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "Katselukerrat vierailua kohti", "label.visit-duration": "Keskimääräinen vierailuaika", "label.visitors": "Vierailijat", - "label.visits": "Visits", - "label.website": "Website", - "label.website-id": "Website ID", + "label.visits": "Vierailut", + "label.website": "Verkkosivu", + "label.website-id": "Verkkosivun ID", "label.websites": "Verkkosivut", - "label.window": "Window", - "label.yesterday": "Yesterday", - "label.behavior": "Behavior", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "label.window": "Ikkuna", + "label.yesterday": "Eilen", + "label.behavior": "Käyttäytyminen", + "message.action-confirmation": "Kirjoita {confirmation} alla olevaan kenttään vahvistaaksesi.", "message.active-users": "{x} {x, plural, one {vierailija} other {vierailijaa}}", - "message.bad-request": "Bad request", - "message.collected-data": "Collected data", + "message.bad-request": "Virheellinen pyyntö", + "message.collected-data": "Kerätty data", "message.confirm-delete": "Haluatko varmasti poistaa sivuston {target}?", - "message.confirm-leave": "Are you sure you want to leave {target}?", - "message.confirm-remove": "Are you sure you want to remove {target}?", + "message.confirm-leave": "Haluatko varmasti poistua {target}?", + "message.confirm-remove": "Haluatko varmasti poistaa {target}?", "message.confirm-reset": "Haluatko varmasti poistaa sivuston {target} tilastot?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.delete-team-warning": "Tiimin poistaminen poistaa myös kaikki tiimin sivustot.", "message.delete-website-warning": "Kaikki siihen liittyvät tiedot poistetaan.", "message.error": "Jotain meni pieleen.", "message.event-log": "{event} on {url}", - "message.forbidden": "Forbidden", + "message.forbidden": "Kielletty", "message.go-to-settings": "Mene asetuksiin", "message.incorrect-username-password": "Väärä käyttäjänimi/salasana.", "message.invalid-domain": "Virheellinen verkkotunnus", - "message.min-password-length": "Minimum length of {n} characters", - "message.new-version-available": "A new version of Umami {version} is available!", + "message.min-password-length": "Vähimmäispituus {n} merkkiä", + "message.new-version-available": "Umamista on saatavilla uusi versio {version}!", "message.no-data-available": "Tietoja ei ole käytettävissä.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Tapahtumatietoja ei ole saatavilla.", "message.no-match-password": "Salasanat eivät täsmää", - "message.no-results-found": "No results were found.", - "message.no-team-websites": "This team does not have any websites.", - "message.no-teams": "You have not created any teams.", - "message.no-users": "There are no users.", + "message.no-results-found": "Tuloksia ei löytynyt.", + "message.no-team-websites": "Tällä tiimillä ei ole verkkosivustoja.", + "message.no-teams": "Et ole luonut yhtään tiimiä.", + "message.no-users": "Käyttäjiä ei ole.", "message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.", - "message.not-found": "Not found", - "message.nothing-selected": "Nothing selected.", + "message.not-found": "Ei löytynyt", + "message.nothing-selected": "Mitään ei ole valittu.", "message.page-not-found": "Sivua ei löydetty.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", + "message.reset-website": "Nollaa verkkosivusto kirjoittamalla {confirmation} alla olevaan kenttään vahvistaaksesi.", "message.reset-website-warning": "Kaikki sivuston tilastot poistetaan, mutta seurantakoodi pysyy muuttumattomana.", "message.saved": "Tallennettu onnistuneesti.", - "message.sever-error": "Server error", + "message.sever-error": "Palvelinvirhe", "message.share-url": "Tämä on julkisesti jaettu URL sivustolle {target}.", - "message.team-already-member": "You are already a member of the team.", - "message.team-not-found": "Team not found.", - "message.team-websites-info": "Websites can be viewed by anyone on the team.", + "message.team-already-member": "Olet jo tiimissä.", + "message.team-not-found": "Tiimiä ei löydetty.", + "message.team-websites-info": "Verkkosivuja voi tarkastella kuka tahansa tiimin jäsen.", "message.tracking-code": "Seurantakoodi", - "message.transfer-team-website-to-user": "Transfer this website to your account?", - "message.transfer-user-website-to-team": "Select the team to transfer this website to.", - "message.transfer-website": "Transfer website ownership to your account or another team.", - "message.triggered-event": "Triggered event", - "message.unauthorized": "Unauthorized", - "message.user-deleted": "User deleted.", - "message.viewed-page": "Viewed page", + "message.transfer-team-website-to-user": "Siirretäänkö tämä verkkosivu tilillesi?", + "message.transfer-user-website-to-team": "Valitse tiimi, johon verkkosivusto siirretään.", + "message.transfer-website": "Siirrä verkkosivusto tiliisi tai toiselle tiimille.", + "message.triggered-event": "Laukaistu tapahtuma", + "message.unauthorized": "Ei oikeuksia", + "message.user-deleted": "Käyttäjä poistettu.", + "message.viewed-page": "Katsottu sivu", "message.visitor-log": "Vierailija maasta {country} selaimella {browser} laitteella {os} {device}" -} +} \ No newline at end of file From 2bd0734162c977ea68d4ce0ac17e9649a8ecdfe6 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 14 Jan 2026 10:28:48 -0800 Subject: [PATCH 02/98] Revert "refactor 6 month retention. use auth instead of cache:website". Fix share page retention bug. This reverts commit 741c6039e60020e62dd81c20f3f01e365d7d2b73. --- src/app/api/reports/attribution/route.ts | 4 ++-- src/app/api/reports/breakdown/route.ts | 4 ++-- src/app/api/reports/funnel/route.ts | 4 ++-- src/app/api/reports/goal/route.ts | 4 ++-- src/app/api/reports/retention/route.ts | 4 ++-- src/app/api/reports/revenue/route.ts | 4 ++-- src/app/api/reports/utm/route.ts | 4 ++-- .../api/websites/[websiteId]/event-data/events/route.ts | 2 +- .../api/websites/[websiteId]/event-data/fields/route.ts | 2 +- .../websites/[websiteId]/event-data/properties/route.ts | 2 +- src/app/api/websites/[websiteId]/event-data/stats/route.ts | 2 +- .../api/websites/[websiteId]/event-data/values/route.ts | 2 +- src/app/api/websites/[websiteId]/events/route.ts | 2 +- src/app/api/websites/[websiteId]/events/series/route.ts | 2 +- src/app/api/websites/[websiteId]/export/route.ts | 2 +- src/app/api/websites/[websiteId]/metrics/expanded/route.ts | 2 +- src/app/api/websites/[websiteId]/metrics/route.ts | 2 +- src/app/api/websites/[websiteId]/pageviews/route.ts | 2 +- .../websites/[websiteId]/session-data/properties/route.ts | 2 +- .../api/websites/[websiteId]/session-data/values/route.ts | 2 +- .../[websiteId]/sessions/[sessionId]/activity/route.ts | 2 +- src/app/api/websites/[websiteId]/sessions/route.ts | 2 +- src/app/api/websites/[websiteId]/sessions/stats/route.ts | 2 +- src/app/api/websites/[websiteId]/sessions/weekly/route.ts | 2 +- src/app/api/websites/[websiteId]/stats/route.ts | 2 +- src/app/api/websites/[websiteId]/values/route.ts | 2 +- src/lib/request.ts | 7 +++---- 27 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/app/api/reports/attribution/route.ts b/src/app/api/reports/attribution/route.ts index ab9b1ddbc..bd7d86dc7 100644 --- a/src/app/api/reports/attribution/route.ts +++ b/src/app/api/reports/attribution/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); const data = await getAttribution(websiteId, parameters as AttributionParameters, filters); diff --git a/src/app/api/reports/breakdown/route.ts b/src/app/api/reports/breakdown/route.ts index a06636c74..3c5931458 100644 --- a/src/app/api/reports/breakdown/route.ts +++ b/src/app/api/reports/breakdown/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters); diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts index f6e210293..c13f6f1c8 100644 --- a/src/app/api/reports/funnel/route.ts +++ b/src/app/api/reports/funnel/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); const data = await getFunnel(websiteId, parameters as FunnelParameters, filters); diff --git a/src/app/api/reports/goal/route.ts b/src/app/api/reports/goal/route.ts index db2aabcef..3bd0415d6 100644 --- a/src/app/api/reports/goal/route.ts +++ b/src/app/api/reports/goal/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); const data = await getGoal(websiteId, parameters as GoalParameters, filters); diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index 5adf7bb85..d1a7d698b 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); + const parameters = await setWebsiteDate(websiteId, body.parameters); const data = await getRetention(websiteId, parameters as RetentionParameters, filters); diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts index f4146b966..6a556612b 100644 --- a/src/app/api/reports/revenue/route.ts +++ b/src/app/api/reports/revenue/route.ts @@ -17,8 +17,8 @@ export async function POST(request: Request) { return unauthorized(); } - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); + const parameters = await setWebsiteDate(websiteId, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); const data = await getRevenue(websiteId, parameters as RevenuParameters, filters); diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts index d4af4da06..577fdab79 100644 --- a/src/app/api/reports/utm/route.ts +++ b/src/app/api/reports/utm/route.ts @@ -18,8 +18,8 @@ export async function POST(request: Request) { return unauthorized(); } - const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id); - const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters); + const filters = await getQueryFilters(body.filters, websiteId); + const parameters = await setWebsiteDate(websiteId, body.parameters); const data = { utm_source: [], diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts index 444afa273..eb6ee6ed8 100644 --- a/src/app/api/websites/[websiteId]/event-data/events/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventDataEvents(websiteId, { ...filters, diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts index e034d9375..bce6a977c 100644 --- a/src/app/api/websites/[websiteId]/event-data/fields/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventDataFields(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts index 29719fb22..52d15cfbd 100644 --- a/src/app/api/websites/[websiteId]/event-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventDataProperties(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts index 1d1e76dff..042e989a4 100644 --- a/src/app/api/websites/[websiteId]/event-data/stats/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventDataStats(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index ed01fb2b6..12e8f2dc0 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -30,7 +30,7 @@ export async function GET( } const { propertyName } = query; - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventDataValues(websiteId, { ...filters, diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts index dfabb87fb..74ec3ece4 100644 --- a/src/app/api/websites/[websiteId]/events/route.ts +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -29,7 +29,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getWebsiteEvents(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index d5b925590..977e9c813 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -29,7 +29,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getEventStats(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts index f9749f34e..eec81c6d4 100644 --- a/src/app/api/websites/[websiteId]/export/route.ts +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -28,7 +28,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([ getEventMetrics(websiteId, { type: 'event' }, filters), diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts index 7e6fbbfdf..d52c17736 100644 --- a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts @@ -37,7 +37,7 @@ export async function GET( } const { type, limit, offset, search } = query; - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); if (search) { filters[type] = `c.${search}`; diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index 2d0e6a600..12784adbe 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -37,7 +37,7 @@ export async function GET( } const { type, limit, offset, search } = query; - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); if (search) { filters[type] = `c.${search}`; diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index dc921bc0c..af59bce46 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const [pageviews, sessions] = await Promise.all([ getPageviewStats(websiteId, filters), diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts index a0aed73ce..2d8db1535 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getSessionDataProperties(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts index db7106548..7d06870ac 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -29,7 +29,7 @@ export async function GET( } const { propertyName } = query; - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getSessionDataValues(websiteId, { ...filters, diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts index 3e70bea0a..41b766d03 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -25,7 +25,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getSessionActivity(websiteId, sessionId, filters); diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts index f344476c3..ed4757a1c 100644 --- a/src/app/api/websites/[websiteId]/sessions/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -28,7 +28,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getWebsiteSessions(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts index 74b4e5e80..459830edf 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const metrics = await getWebsiteSessionStats(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts index f6dfd206b..b9ccf3ef0 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -28,7 +28,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getWeeklyTraffic(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 2bf862cdc..07c8b9699 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -27,7 +27,7 @@ export async function GET( return unauthorized(); } - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); const data = await getWebsiteStats(websiteId, filters); diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index c2c95b375..172325e3f 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -42,7 +42,7 @@ export async function GET( value: segment.name, })); } else { - const filters = await getQueryFilters(query, websiteId, auth.user?.id); + const filters = await getQueryFilters(query, websiteId); values = await getValues(websiteId, FILTER_COLUMNS[type], filters); } diff --git a/src/lib/request.ts b/src/lib/request.ts index d6543b184..7f9163cca 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -81,12 +81,12 @@ export function getRequestFilters(query: Record) { return result; } -export async function setWebsiteDate(websiteId: string, userId: string, data: Record) { +export async function setWebsiteDate(websiteId: string, data: Record) { const website = await fetchWebsite(websiteId); const cloudMode = !!process.env.CLOUD_MODE; if (cloudMode && website && !website.teamId) { - const account = await fetchAccount(userId); + const account = await fetchAccount(website.userId); if (!account?.hasSubscription) { data.startDate = maxDate(data.startDate, startOfMonth(subMonths(new Date(), 6))); @@ -103,13 +103,12 @@ export async function setWebsiteDate(websiteId: string, userId: string, data: Re export async function getQueryFilters( params: Record, websiteId?: string, - userId?: string, ): Promise { const dateRange = getRequestDateRange(params); const filters = getRequestFilters(params); if (websiteId) { - await setWebsiteDate(websiteId, userId, dateRange); + await setWebsiteDate(websiteId, dateRange); if (params.segment) { const segmentParams = (await getWebsiteSegment(websiteId, params.segment)) From cd6a28ea4a7fafd1dc352447430ff1a00ba82933 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 19 Jan 2026 12:46:21 -0800 Subject: [PATCH 03/98] improve revenue joins and event_data joins to website_event --- src/queries/sql/events/getEventDataEvents.ts | 20 +++++++--- src/queries/sql/events/getEventDataFields.ts | 10 +++-- .../sql/events/getEventDataProperties.ts | 10 +++-- src/queries/sql/events/getEventDataStats.ts | 10 +++-- src/queries/sql/events/getEventDataValues.ts | 10 +++-- src/queries/sql/reports/getRevenue.ts | 38 +++++++++++-------- 6 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/queries/sql/events/getEventDataEvents.ts b/src/queries/sql/events/getEventDataEvents.ts index 6c8f12c19..8ed6633ea 100644 --- a/src/queries/sql/events/getEventDataEvents.ts +++ b/src/queries/sql/events/getEventDataEvents.ts @@ -93,11 +93,15 @@ async function clickhouseQuery( string_value as propertyValue, count(*) as total from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} @@ -120,11 +124,15 @@ async function clickhouseQuery( data_type as dataType, count(*) as total from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/sql/events/getEventDataFields.ts b/src/queries/sql/events/getEventDataFields.ts index 933776909..709454929 100644 --- a/src/queries/sql/events/getEventDataFields.ts +++ b/src/queries/sql/events/getEventDataFields.ts @@ -65,11 +65,15 @@ async function clickhouseQuery( string_value) as "value", count(*) as "total" from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/sql/events/getEventDataProperties.ts b/src/queries/sql/events/getEventDataProperties.ts index 82c078f8d..82d0b6b62 100644 --- a/src/queries/sql/events/getEventDataProperties.ts +++ b/src/queries/sql/events/getEventDataProperties.ts @@ -69,11 +69,15 @@ async function clickhouseQuery( data_key as propertyName, count(*) as total from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/sql/events/getEventDataStats.ts b/src/queries/sql/events/getEventDataStats.ts index 89e135828..9d178f676 100644 --- a/src/queries/sql/events/getEventDataStats.ts +++ b/src/queries/sql/events/getEventDataStats.ts @@ -72,11 +72,15 @@ async function clickhouseQuery( data_key, count(*) as "total" from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/sql/events/getEventDataValues.ts b/src/queries/sql/events/getEventDataValues.ts index 0426e646e..7c1ae282e 100644 --- a/src/queries/sql/events/getEventDataValues.ts +++ b/src/queries/sql/events/getEventDataValues.ts @@ -72,11 +72,15 @@ async function clickhouseQuery( string_value) as "value", count(*) as "total" from event_data - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.event_id = event_data.event_id + and website_event.session_id = event_data.session_id and website_event.website_id = event_data.website_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where event_data.website_id = {websiteId:UUID} and event_data.created_at between {startDate:DateTime64} and {endDate:DateTime64} diff --git a/src/queries/sql/reports/getRevenue.ts b/src/queries/sql/reports/getRevenue.ts index 2a4604fec..d3bff6c9a 100644 --- a/src/queries/sql/reports/getRevenue.ts +++ b/src/queries/sql/reports/getRevenue.ts @@ -43,12 +43,14 @@ async function relationalQuery( const joinQuery = filterQuery || cohortQuery - ? `join website_event - on website_event.website_id = revenue.website_id - and website_event.session_id = revenue.session_id - and website_event.event_id = revenue.event_id - and website_event.website_id = {{websiteId::uuid}} - and website_event.created_at between {{startDate}} and {{endDate}}` + ? `join (select * + from website_event + where website_id = {{websiteId::uuid}} + and created_at between {{startDate}} and {{endDate}} + and event_type = 2) website_event + on website_event.website_id = revenue.website_id + and website_event.session_id = revenue.session_id + and website_event.event_id = revenue.event_id` : ''; const chart = await rawQuery( @@ -130,12 +132,15 @@ async function clickhouseQuery( }); const joinQuery = filterQuery - ? `join website_event - on website_event.website_id = website_revenue.website_id - and website_event.session_id = website_revenue.session_id - and website_event.event_id = website_revenue.event_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64}` + ? `any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event + on website_event.website_id = website_revenue.website_id + and website_event.session_id = website_revenue.session_id + and website_event.event_id = website_revenue.event_id` : ''; const chart = await rawQuery< @@ -174,12 +179,15 @@ async function clickhouseQuery( website_event.country as name, sum(website_revenue.revenue) as value from website_revenue - join website_event + any left join ( + select * + from website_event + where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} + and event_type = 2) website_event on website_event.website_id = website_revenue.website_id and website_event.session_id = website_revenue.session_id and website_event.event_id = website_revenue.event_id - and website_event.website_id = {websiteId:UUID} - and website_event.created_at between {startDate:DateTime64} and {endDate:DateTime64} ${cohortQuery} where website_revenue.website_id = {websiteId:UUID} and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64} From 3fe7afbb12b3dc6915e041f24408248394df91fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 01:20:18 +0000 Subject: [PATCH 04/98] Bump tar from 6.2.1 to 7.5.4 Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.1 to 7.5.4. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.1...v7.5.4) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 78 ++++++++++++++++++++++---------------------------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 76b1e1fa9..456d1a2c8 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "stylelint-config-css-modules": "^4.5.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^14.0.0", - "tar": "^6.1.2", + "tar": "^7.5.4", "ts-jest": "^29.4.6", "ts-node": "^10.9.1", "tsup": "^8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f2b1ce65..a042f9ee2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,8 +313,8 @@ importers: specifier: ^14.0.0 version: 14.0.1(stylelint@15.11.0(typescript@5.9.3)) tar: - specifier: ^6.1.2 - version: 6.2.1 + specifier: ^7.5.4 + version: 7.5.4 ts-jest: specifier: ^29.4.6 version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) @@ -1567,6 +1567,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -3318,9 +3322,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} @@ -4112,10 +4116,6 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -5195,21 +5195,13 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -6755,9 +6747,9 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} + tar@7.5.4: + resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} + engines: {node: '>=18'} terser@5.43.1: resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} @@ -7156,6 +7148,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -8249,6 +8245,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 @@ -10569,7 +10569,7 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@2.0.0: {} + chownr@3.0.0: {} ci-info@3.9.0: {} @@ -11548,10 +11548,6 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -12833,18 +12829,11 @@ snapshots: minimist@1.2.8: {} - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} - minipass@7.1.2: {} - minizlib@2.1.2: + minizlib@3.1.0: dependencies: - minipass: 3.3.6 - yallist: 4.0.0 + minipass: 7.1.2 mkdirp@1.0.4: {} @@ -14636,14 +14625,13 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tar@6.2.1: + tar@7.5.4: dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 terser@5.43.1: dependencies: @@ -15058,6 +15046,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml@1.10.2: {} yaml@2.8.1: {} From 0eb598c817157fa32291ec74d4619d5a81ef7bf0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 20 Jan 2026 16:45:02 -0800 Subject: [PATCH 05/98] implement website share functionality using share table - Add support for multiple share URLs per website with server-generated slugs - Create shares API endpoint for listing and creating website shares - Add SharesTable, ShareEditButton, ShareDeleteButton components - Move share management to website settings, remove header share button - Remove shareId from website update API (now uses separate share table) Co-Authored-By: Claude Opus 4.5 --- .../websites/[websiteId]/WebsiteHeader.tsx | 31 ++--- .../settings/ShareDeleteButton.tsx | 57 +++++++++ .../[websiteId]/settings/ShareEditButton.tsx | 16 +++ .../[websiteId]/settings/ShareEditForm.tsx | 94 +++++++++++++++ .../[websiteId]/settings/SharesTable.tsx | 46 +++++++ .../[websiteId]/settings/WebsiteSettings.tsx | 5 +- .../[websiteId]/settings/WebsiteShareForm.tsx | 112 +++++------------- src/app/api/share/route.ts | 5 +- src/app/api/websites/[websiteId]/route.ts | 18 +-- .../api/websites/[websiteId]/shares/route.ts | 74 ++++++++++++ src/components/hooks/index.ts | 1 + .../hooks/queries/useWebsiteSharesQuery.ts | 20 ++++ src/queries/prisma/share.ts | 22 +++- 13 files changed, 374 insertions(+), 127 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx create mode 100644 src/app/api/websites/[websiteId]/shares/route.ts create mode 100644 src/components/hooks/queries/useWebsiteSharesQuery.ts diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index 7dd1d771c..e79576dd3 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -1,11 +1,9 @@ import { Icon, Row, Text } from '@umami/react-zen'; -import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm'; import { Favicon } from '@/components/common/Favicon'; import { LinkButton } from '@/components/common/LinkButton'; import { PageHeader } from '@/components/common/PageHeader'; import { useMessages, useNavigation, useWebsite } from '@/components/hooks'; -import { Edit, Share } from '@/components/icons'; -import { DialogButton } from '@/components/input/DialogButton'; +import { Edit } from '@/components/icons'; import { ActiveUsers } from '@/components/metrics/ActiveUsers'; export function WebsiteHeader({ showActions }: { showActions?: boolean }) { @@ -29,29 +27,14 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) { {showActions && ( - - - - - - - {formatMessage(labels.edit)} - - + + + + + {formatMessage(labels.edit)} + )} ); } - -const ShareButton = ({ websiteId, shareId }) => { - const { formatMessage, labels } = useMessages(); - - return ( - } label={formatMessage(labels.share)} width="800px"> - {({ close }) => { - return ; - }} - - ); -}; diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx new file mode 100644 index 000000000..35e96df3f --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/ShareDeleteButton.tsx @@ -0,0 +1,57 @@ +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { useDeleteQuery, useMessages, useModified } from '@/components/hooks'; +import { Trash } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { messages } from '@/components/messages'; + +export function ShareDeleteButton({ + shareId, + slug, + onSave, +}: { + shareId: string; + slug: string; + onSave?: () => void; +}) { + const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages(); + const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${shareId}`); + const { touch } = useModified(); + + const handleConfirm = async (close: () => void) => { + await mutateAsync(null, { + onSuccess: () => { + touch('shares'); + onSave?.(); + close(); + }, + }); + }; + + return ( + } + title={formatMessage(labels.confirm)} + variant="quiet" + width="400px" + > + {({ close }) => ( + {slug}, + }} + /> + } + isLoading={isPending} + error={getErrorMessage(error)} + onConfirm={handleConfirm.bind(null, close)} + onClose={close} + buttonLabel={formatMessage(labels.delete)} + buttonVariant="danger" + /> + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx new file mode 100644 index 000000000..df1c2e648 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditButton.tsx @@ -0,0 +1,16 @@ +import { useMessages } from '@/components/hooks'; +import { Edit } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { ShareEditForm } from './ShareEditForm'; + +export function ShareEditButton({ shareId }: { shareId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + } title={formatMessage(labels.share)} variant="quiet" width="600px"> + {({ close }) => { + return ; + }} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx new file mode 100644 index 000000000..b1d7d50a5 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -0,0 +1,94 @@ +import { + Button, + Column, + Form, + FormSubmitButton, + Label, + Loading, + Row, + TextField, +} from '@umami/react-zen'; +import { useEffect, useState } from 'react'; +import { useApi, useConfig, useMessages, useModified } from '@/components/hooks'; +import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; + +export function ShareEditForm({ + shareId, + onSave, + onClose, +}: { + shareId: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels, messages, getErrorMessage } = useMessages(); + const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`); + const { cloudMode } = useConfig(); + const { get } = useApi(); + const { modified } = useModified('shares'); + const [share, setShare] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const getUrl = (slug: string) => { + if (cloudMode) { + return `${process.env.cloudUrl}/share/${slug}`; + } + return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + }; + + useEffect(() => { + const loadShare = async () => { + setIsLoading(true); + try { + const data = await get(`/share/id/${shareId}`); + setShare(data); + } finally { + setIsLoading(false); + } + }; + loadShare(); + }, [shareId, modified]); + + const handleSubmit = async (data: any) => { + await mutateAsync( + { slug: data.slug, parameters: share?.parameters || {} }, + { + onSuccess: async () => { + toast(formatMessage(messages.saved)); + touch('shares'); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (isLoading) { + return ; + } + + const url = getUrl(share?.slug || ''); + + return ( +
+ + + + + + + {onClose && ( + + )} + {formatMessage(labels.save)} + + +
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx new file mode 100644 index 000000000..05e8b357b --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx @@ -0,0 +1,46 @@ +import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; +import { DateDistance } from '@/components/common/DateDistance'; +import { ExternalLink } from '@/components/common/ExternalLink'; +import { useConfig, useMessages } from '@/components/hooks'; +import { ShareDeleteButton } from './ShareDeleteButton'; +import { ShareEditButton } from './ShareEditButton'; + +export function SharesTable(props: DataTableProps) { + const { formatMessage, labels } = useMessages(); + const { cloudMode } = useConfig(); + + const getUrl = (slug: string) => { + if (cloudMode) { + return `${process.env.cloudUrl}/share/${slug}`; + } + return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + }; + + return ( + + + {({ slug }: any) => { + const url = getUrl(slug); + return ( + + {url} + + ); + }} + + + {(row: any) => } + + + {({ id, slug }: any) => { + return ( + + + + + ); + }} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx index 3970cdbdb..d39c45315 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteSettings.tsx @@ -1,14 +1,11 @@ import { Column } from '@umami/react-zen'; import { Panel } from '@/components/common/Panel'; -import { useWebsite } from '@/components/hooks'; import { WebsiteData } from './WebsiteData'; import { WebsiteEditForm } from './WebsiteEditForm'; import { WebsiteShareForm } from './WebsiteShareForm'; import { WebsiteTrackingCode } from './WebsiteTrackingCode'; export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) { - const website = useWebsite(); - return ( @@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal - + diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx index 56c6f436f..6ac4a4048 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx @@ -1,93 +1,43 @@ -import { - Button, - Column, - Form, - FormButtons, - FormSubmitButton, - IconLabel, - Label, - Row, - Switch, - TextField, -} from '@umami/react-zen'; -import { RefreshCcw } from 'lucide-react'; -import { useState } from 'react'; -import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks'; -import { getRandomChars } from '@/lib/generate'; - -const generateId = () => getRandomChars(16); +import { Button, Column, Heading, Row, Text } from '@umami/react-zen'; +import { Plus } from 'lucide-react'; +import { useApi, useMessages, useModified, useWebsiteSharesQuery } from '@/components/hooks'; +import { SharesTable } from './SharesTable'; export interface WebsiteShareFormProps { websiteId: string; - shareId?: string; - onSave?: () => void; - onClose?: () => void; } -export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) { - const { formatMessage, labels, messages, getErrorMessage } = useMessages(); - const [currentId, setCurrentId] = useState(shareId); - const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`); - const { cloudMode } = useConfig(); +export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) { + const { formatMessage, labels, messages } = useMessages(); + const { data, isLoading } = useWebsiteSharesQuery({ websiteId }); + const { post } = useApi(); + const { touch } = useModified(); - const getUrl = (shareId: string) => { - if (cloudMode) { - return `${process.env.cloudUrl}/share/${shareId}`; - } - - return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`; + const handleCreate = async () => { + await post(`/websites/${websiteId}/shares`, { parameters: {} }); + touch('shares'); }; - const url = getUrl(currentId); - - const handleGenerate = () => { - setCurrentId(generateId()); - }; - - const handleSwitch = () => { - setCurrentId(currentId ? null : generateId()); - }; - - const handleSave = async () => { - const data = { - shareId: currentId, - }; - await mutateAsync(data, { - onSuccess: async () => { - toast(formatMessage(messages.saved)); - touch(`website:${websiteId}`); - onSave?.(); - onClose?.(); - }, - }); - }; + const shares = data?.data || []; + const hasShares = shares.length > 0; return ( -
- - - {formatMessage(labels.enableShareUrl)} - - {currentId && ( - - - - - - - - - - )} - - - {onClose && } - {formatMessage(labels.save)} - - - -
+ + + {formatMessage(labels.share)} + + + {hasShares ? ( + <> + {formatMessage(messages.shareUrl)} + + + ) : ( + {formatMessage(messages.noDataAvailable)} + )} + ); } diff --git a/src/app/api/share/route.ts b/src/app/api/share/route.ts index 99f5df0ef..a772b4ab4 100644 --- a/src/app/api/share/route.ts +++ b/src/app/api/share/route.ts @@ -1,5 +1,6 @@ import z from 'zod'; import { uuid } from '@/lib/crypto'; +import { getRandomChars } from '@/lib/generate'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { anyObjectParam } from '@/lib/schema'; @@ -10,7 +11,7 @@ export async function POST(request: Request) { const schema = z.object({ entityId: z.uuid(), shareType: z.coerce.number().int(), - slug: z.string().max(100), + slug: z.string().max(100).optional(), parameters: anyObjectParam, }); @@ -30,7 +31,7 @@ export async function POST(request: Request) { id: uuid(), entityId, shareType, - slug, + slug: slug || getRandomChars(16), parameters, }); diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts index b4c0e7e88..59f314d38 100644 --- a/src/app/api/websites/[websiteId]/route.ts +++ b/src/app/api/websites/[websiteId]/route.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; -import { SHARE_ID_REGEX } from '@/lib/constants'; import { parseRequest } from '@/lib/request'; -import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response'; +import { json, ok, unauthorized } from '@/lib/response'; import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions'; import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma'; @@ -33,7 +32,6 @@ export async function POST( const schema = z.object({ name: z.string().optional(), domain: z.string().optional(), - shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -43,23 +41,15 @@ export async function POST( } const { websiteId } = await params; - const { name, domain, shareId } = body; + const { name, domain } = body; if (!(await canUpdateWebsite(auth, websiteId))) { return unauthorized(); } - try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); + const website = await updateWebsite(websiteId, { name, domain }); - return Response.json(website); - } catch (e: any) { - if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) { - return badRequest({ message: 'That share ID is already taken.' }); - } - - return serverError(e); - } + return Response.json(website); } export async function DELETE( diff --git a/src/app/api/websites/[websiteId]/shares/route.ts b/src/app/api/websites/[websiteId]/shares/route.ts new file mode 100644 index 000000000..db079d497 --- /dev/null +++ b/src/app/api/websites/[websiteId]/shares/route.ts @@ -0,0 +1,74 @@ +import { z } from 'zod'; +import { ENTITY_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { getRandomChars } from '@/lib/generate'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema'; +import { canUpdateWebsite, canViewWebsite } from '@/permissions'; +import { createShare, getSharesByEntityId } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const data = await getSharesByEntityId(websiteId, { + page, + pageSize, + search, + }); + + return json(data); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + parameters: anyObjectParam.optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { parameters = {} } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + const slug = getRandomChars(16); + + const share = await createShare({ + id: uuid(), + entityId: websiteId, + shareType: ENTITY_TYPE.website, + slug, + parameters, + }); + + return json(share); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index e8e5c135e..f47f11f02 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -51,6 +51,7 @@ export * from './queries/useWebsiteSegmentsQuery'; export * from './queries/useWebsiteSessionQuery'; export * from './queries/useWebsiteSessionStatsQuery'; export * from './queries/useWebsiteSessionsQuery'; +export * from './queries/useWebsiteSharesQuery'; export * from './queries/useWebsiteStatsQuery'; export * from './queries/useWebsitesQuery'; export * from './queries/useWebsiteValuesQuery'; diff --git a/src/components/hooks/queries/useWebsiteSharesQuery.ts b/src/components/hooks/queries/useWebsiteSharesQuery.ts new file mode 100644 index 000000000..298e4d264 --- /dev/null +++ b/src/components/hooks/queries/useWebsiteSharesQuery.ts @@ -0,0 +1,20 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; + +export function useWebsiteSharesQuery( + { websiteId }: { websiteId: string }, + options?: ReactQueryOptions, +) { + const { modified } = useModified('shares'); + const { get } = useApi(); + + return usePagedQuery({ + queryKey: ['websiteShares', { websiteId, modified }], + queryFn: pageParams => { + return get(`/websites/${websiteId}/shares`, pageParams); + }, + ...options, + }); +} diff --git a/src/queries/prisma/share.ts b/src/queries/prisma/share.ts index e37dc95b6..53246ffbe 100644 --- a/src/queries/prisma/share.ts +++ b/src/queries/prisma/share.ts @@ -1,14 +1,15 @@ import type { Prisma } from '@/generated/prisma/client'; import prisma from '@/lib/prisma'; +import type { QueryFilters } from '@/lib/types'; export async function findShare(criteria: Prisma.ShareFindUniqueArgs) { return prisma.client.share.findUnique(criteria); } -export async function getShare(entityId: string) { +export async function getShare(shareId: string) { return findShare({ where: { - id: entityId, + id: shareId, }, }); } @@ -21,6 +22,23 @@ export async function getShareByCode(slug: string) { }); } +export async function getSharesByEntityId(entityId: string, filters?: QueryFilters) { + const { pagedQuery } = prisma; + + return pagedQuery( + 'share', + { + where: { + entityId, + }, + orderBy: { + createdAt: 'desc', + }, + }, + filters, + ); +} + export async function createShare( data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput, ) { From ef3aec09bea636100dd2a2fa801f6cd1357ada6e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 20 Jan 2026 17:22:16 -0800 Subject: [PATCH 06/98] Add display options form for website shares Allow users to select which navigation items to display when creating or editing a share. Options include traffic, behavior, and growth sections with checkboxes for each nav item (excluding segments/cohorts). Co-Authored-By: Claude Opus 4.5 --- .../[websiteId]/settings/ShareCreateForm.tsx | 83 +++++++++++++++++++ .../[websiteId]/settings/ShareEditForm.tsx | 45 ++++++++-- .../[websiteId]/settings/WebsiteShareForm.tsx | 26 +++--- .../[websiteId]/settings/constants.ts | 30 +++++++ 4 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/settings/constants.ts diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx new file mode 100644 index 000000000..024fc10b7 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/ShareCreateForm.tsx @@ -0,0 +1,83 @@ +import { + Button, + Checkbox, + Column, + Form, + FormField, + FormSubmitButton, + Row, + Text, +} from '@umami/react-zen'; +import { useState } from 'react'; +import { useApi, useMessages, useModified } from '@/components/hooks'; +import { SHARE_NAV_ITEMS } from './constants'; + +export interface ShareCreateFormProps { + websiteId: string; + onSave?: () => void; + onClose?: () => void; +} + +export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) { + const { formatMessage, labels } = useMessages(); + const { post } = useApi(); + const { touch } = useModified(); + const [isPending, setIsPending] = useState(false); + + // Build default values - all enabled by default + const defaultValues: Record = {}; + SHARE_NAV_ITEMS.forEach(section => { + section.items.forEach(item => { + defaultValues[item.id] = true; + }); + }); + + const handleSubmit = async (data: any) => { + setIsPending(true); + try { + const parameters: Record = {}; + SHARE_NAV_ITEMS.forEach(section => { + section.items.forEach(item => { + parameters[item.id] = data[item.id] ?? true; + }); + }); + await post(`/websites/${websiteId}/shares`, { parameters }); + touch('shares'); + onSave?.(); + onClose?.(); + } finally { + setIsPending(false); + } + }; + + return ( +
+ + {SHARE_NAV_ITEMS.map(section => ( + + + {formatMessage((labels as any)[section.section])} + + + {section.items.map(item => ( + + + {formatMessage((labels as any)[item.label])} + + + ))} + + + ))} + + {onClose && ( + + )} + {formatMessage(labels.save)} + + +
+ ); +} diff --git a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx index b1d7d50a5..5e8f8a74e 100644 --- a/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx @@ -1,16 +1,20 @@ import { Button, + Checkbox, Column, Form, + FormField, FormSubmitButton, Label, Loading, Row, + Text, TextField, } from '@umami/react-zen'; import { useEffect, useState } from 'react'; import { useApi, useConfig, useMessages, useModified } from '@/components/hooks'; import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; +import { SHARE_NAV_ITEMS } from './constants'; export function ShareEditForm({ shareId, @@ -50,8 +54,15 @@ export function ShareEditForm({ }, [shareId, modified]); const handleSubmit = async (data: any) => { + const parameters: Record = {}; + SHARE_NAV_ITEMS.forEach(section => { + section.items.forEach(item => { + parameters[item.id] = data[item.id] ?? true; + }); + }); + await mutateAsync( - { slug: data.slug, parameters: share?.parameters || {} }, + { slug: share.slug, parameters }, { onSuccess: async () => { toast(formatMessage(messages.saved)); @@ -69,24 +80,42 @@ export function ShareEditForm({ const url = getUrl(share?.slug || ''); + // Build default values from share parameters + const defaultValues: Record = {}; + SHARE_NAV_ITEMS.forEach(section => { + section.items.forEach(item => { + defaultValues[item.id] = share?.parameters?.[item.id] ?? true; + }); + }); + return ( -
- + + + {SHARE_NAV_ITEMS.map(section => ( + + + {formatMessage((labels as any)[section.section])} + + + {section.items.map(item => ( + + {formatMessage((labels as any)[item.label])} + + ))} + + + ))} {onClose && ( )} - {formatMessage(labels.save)} + {formatMessage(labels.save)} diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx index 6ac4a4048..7453b4028 100644 --- a/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx +++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteShareForm.tsx @@ -1,6 +1,8 @@ -import { Button, Column, Heading, Row, Text } from '@umami/react-zen'; +import { Column, Heading, Row, Text } from '@umami/react-zen'; import { Plus } from 'lucide-react'; -import { useApi, useMessages, useModified, useWebsiteSharesQuery } from '@/components/hooks'; +import { useMessages, useWebsiteSharesQuery } from '@/components/hooks'; +import { DialogButton } from '@/components/input/DialogButton'; +import { ShareCreateForm } from './ShareCreateForm'; import { SharesTable } from './SharesTable'; export interface WebsiteShareFormProps { @@ -10,13 +12,6 @@ export interface WebsiteShareFormProps { export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) { const { formatMessage, labels, messages } = useMessages(); const { data, isLoading } = useWebsiteSharesQuery({ websiteId }); - const { post } = useApi(); - const { touch } = useModified(); - - const handleCreate = async () => { - await post(`/websites/${websiteId}/shares`, { parameters: {} }); - touch('shares'); - }; const shares = data?.data || []; const hasShares = shares.length > 0; @@ -25,10 +20,15 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) { {formatMessage(labels.share)} - + } + label={formatMessage(labels.add)} + title={formatMessage(labels.share)} + variant="primary" + width="400px" + > + {({ close }) => } + {hasShares ? ( <> diff --git a/src/app/(main)/websites/[websiteId]/settings/constants.ts b/src/app/(main)/websites/[websiteId]/settings/constants.ts new file mode 100644 index 000000000..f4a3df80f --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/settings/constants.ts @@ -0,0 +1,30 @@ +export const SHARE_NAV_ITEMS = [ + { + section: 'traffic', + items: [ + { id: 'overview', label: 'overview' }, + { id: 'events', label: 'events' }, + { id: 'sessions', label: 'sessions' }, + { id: 'realtime', label: 'realtime' }, + { id: 'compare', label: 'compare' }, + { id: 'breakdown', label: 'breakdown' }, + ], + }, + { + section: 'behavior', + items: [ + { id: 'goals', label: 'goals' }, + { id: 'funnels', label: 'funnels' }, + { id: 'journeys', label: 'journeys' }, + { id: 'retention', label: 'retention' }, + ], + }, + { + section: 'growth', + items: [ + { id: 'utm', label: 'utm' }, + { id: 'revenue', label: 'revenue' }, + { id: 'attribution', label: 'attribution' }, + ], + }, +]; From f2c49845d03e31a0e4321b221dbc509febda7fd5 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 20 Jan 2026 18:12:33 -0800 Subject: [PATCH 07/98] Add filtered navigation to share pages - Update share API to return websiteId and parameters - Create ShareNav component that filters nav items based on parameters - Update SharePage to include navigation sidebar and route to correct page - Support all website pages: overview, events, sessions, realtime, compare, breakdown, goals, funnels, journeys, retention, utm, revenue, attribution Co-Authored-By: Claude Opus 4.5 --- src/app/api/share/[slug]/route.ts | 6 +- src/app/share/[...shareId]/ShareNav.tsx | 143 +++++++++++++++++++++++ src/app/share/[...shareId]/SharePage.tsx | 76 ++++++++++-- src/app/share/[...shareId]/page.tsx | 3 +- 4 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 src/app/share/[...shareId]/ShareNav.tsx diff --git a/src/app/api/share/[slug]/route.ts b/src/app/api/share/[slug]/route.ts index 678795e07..ed3271eab 100644 --- a/src/app/api/share/[slug]/route.ts +++ b/src/app/api/share/[slug]/route.ts @@ -12,7 +12,11 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu return notFound(); } - const data = { shareId: share.id }; + const data = { + shareId: share.id, + websiteId: share.entityId, + parameters: share.parameters, + }; const token = createToken(data, secret()); return json({ ...data, token }); diff --git a/src/app/share/[...shareId]/ShareNav.tsx b/src/app/share/[...shareId]/ShareNav.tsx new file mode 100644 index 000000000..b494046d4 --- /dev/null +++ b/src/app/share/[...shareId]/ShareNav.tsx @@ -0,0 +1,143 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { SideMenu } from '@/components/common/SideMenu'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons'; +import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg'; + +export function ShareNav({ + shareId, + parameters, + onItemClick, +}: { + shareId: string; + parameters: Record; + onItemClick?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { pathname } = useNavigation(); + + const renderPath = (path: string) => `/share/${shareId}${path}`; + + const allItems = [ + { + section: 'traffic', + label: formatMessage(labels.traffic), + items: [ + { + id: 'overview', + label: formatMessage(labels.overview), + icon: , + path: renderPath(''), + }, + { + id: 'events', + label: formatMessage(labels.events), + icon: , + path: renderPath('/events'), + }, + { + id: 'sessions', + label: formatMessage(labels.sessions), + icon: , + path: renderPath('/sessions'), + }, + { + id: 'realtime', + label: formatMessage(labels.realtime), + icon: , + path: renderPath('/realtime'), + }, + { + id: 'compare', + label: formatMessage(labels.compare), + icon: , + path: renderPath('/compare'), + }, + { + id: 'breakdown', + label: formatMessage(labels.breakdown), + icon: , + path: renderPath('/breakdown'), + }, + ], + }, + { + section: 'behavior', + label: formatMessage(labels.behavior), + items: [ + { + id: 'goals', + label: formatMessage(labels.goals), + icon: , + path: renderPath('/goals'), + }, + { + id: 'funnels', + label: formatMessage(labels.funnels), + icon: , + path: renderPath('/funnels'), + }, + { + id: 'journeys', + label: formatMessage(labels.journeys), + icon: , + path: renderPath('/journeys'), + }, + { + id: 'retention', + label: formatMessage(labels.retention), + icon: , + path: renderPath('/retention'), + }, + ], + }, + { + section: 'growth', + label: formatMessage(labels.growth), + items: [ + { + id: 'utm', + label: formatMessage(labels.utm), + icon: , + path: renderPath('/utm'), + }, + { + id: 'revenue', + label: formatMessage(labels.revenue), + icon: , + path: renderPath('/revenue'), + }, + { + id: 'attribution', + label: formatMessage(labels.attribution), + icon: , + path: renderPath('/attribution'), + }, + ], + }, + ]; + + // Filter items based on parameters + const items = allItems + .map(section => ({ + label: section.label, + items: section.items.filter(item => parameters[item.id] !== false), + })) + .filter(section => section.items.length > 0); + + const selectedKey = items + .flatMap(e => e.items) + .find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id; + + return ( + + + + ); +} diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index 7ed066735..3e1cedc06 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -1,6 +1,18 @@ 'use client'; -import { Column, useTheme } from '@umami/react-zen'; +import { Column, Grid, useTheme } from '@umami/react-zen'; import { useEffect } from 'react'; +import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage'; +import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage'; +import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage'; +import { GoalsPage } from '@/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage'; +import { JourneysPage } from '@/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage'; +import { RetentionPage } from '@/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage'; +import { RevenuePage } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage'; +import { UTMPage } from '@/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage'; +import { ComparePage } from '@/app/(main)/websites/[websiteId]/compare/ComparePage'; +import { EventsPage } from '@/app/(main)/websites/[websiteId]/events/EventsPage'; +import { RealtimePage } from '@/app/(main)/websites/[websiteId]/realtime/RealtimePage'; +import { SessionsPage } from '@/app/(main)/websites/[websiteId]/sessions/SessionsPage'; import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader'; import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; @@ -8,8 +20,26 @@ import { PageBody } from '@/components/common/PageBody'; import { useShareTokenQuery } from '@/components/hooks'; import { Footer } from './Footer'; import { Header } from './Header'; +import { ShareNav } from './ShareNav'; -export function SharePage({ shareId }) { +const PAGE_COMPONENTS: Record> = { + '': WebsitePage, + overview: WebsitePage, + events: EventsPage, + sessions: SessionsPage, + realtime: RealtimePage, + compare: ComparePage, + breakdown: BreakdownPage, + goals: GoalsPage, + funnels: FunnelsPage, + journeys: JourneysPage, + retention: RetentionPage, + utm: UTMPage, + revenue: RevenuePage, + attribution: AttributionPage, +}; + +export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) { const { shareToken, isLoading } = useShareTokenQuery(shareId); const { setTheme } = useTheme(); @@ -26,16 +56,42 @@ export function SharePage({ shareId }) { return null; } + const { websiteId, parameters = {} } = shareToken; + + // Check if the requested path is allowed + const pageKey = path || ''; + const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false; + + if (!isAllowed) { + return null; + } + + const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage; + return ( - -
- - - - -