Skip to content
Merged
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
42 changes: 33 additions & 9 deletions apps/console/app/install-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,30 @@ export function snippetFor(site: Site): string {
const CONNECT_PROBLEMS: Readonly<Record<string, string>> = {};
const BROWSER_NOT_CONNECTED = msg('This browser was not connected. Try again.');

export function InstallOptions({ workspace, site }: { workspace: Workspace; site: Site }) {
useLocale();
/**
* Where the extension is explained. It is in no store yet (FRU-130): until it is, « Add to Chrome »
* and « Get the Firefox add-on » lead to the page that says how to install it.
*/
export const EXTENSION_GUIDE = 'https://fruitback.com/reviewing.html#1-install-it';

/** A text put in the clipboard: whether the browser took it. « refused »: it stays on screen to select. */
export function useCopy(text: string): { copied: 'no' | 'yes' | 'refused'; copy: () => void } {
const [copied, setCopied] = useState<'no' | 'yes' | 'refused'>('no');

return { copied, copy: () => void copyText(text).then((taken) => setCopied(taken ? 'yes' : 'refused')) };
}

/**
* Connects the browser of the person: a code is made, and the pairing page of the worker opens with
* it in another tab. `link` is that page, for a browser that blocked the tab.
*/
export function useConnect(workspace: Workspace): {
link: string | undefined;
problem: string | undefined;
connect: () => void;
} {
const [link, setLink] = useState<string | undefined>();
const [problem, setProblem] = useState<string | undefined>();
const snippet = snippetFor(site);

async function connect() {
// Open the tab in the click, before the await: a browser blocks a window opened after one.
Expand All @@ -42,6 +60,7 @@ export function InstallOptions({ workspace, site }: { workspace: Workspace; site
if (!minted.ok) {
// The tab that just opened closes: the reason is said under the button (FRU-164).
tab?.close();

return setProblem(problemOf(minted, CONNECT_PROBLEMS, BROWSER_NOT_CONNECTED));
}
const target = pairingLink(minted.data.code);
Expand All @@ -52,18 +71,23 @@ export function InstallOptions({ workspace, site }: { workspace: Workspace; site
}
}

return { link, problem, connect: () => void connect() };
}

export function InstallOptions({ workspace, site }: { workspace: Workspace; site: Site }) {
useLocale();
const snippet = snippetFor(site);
const { copied, copy } = useCopy(snippet);
const { link, problem, connect } = useConnect(workspace);

return (
<div className="space-y-5">
<div>
<h2 className="text-sm font-semibold">{t('Script')}</h2>
<p className="mb-2 text-xs text-muted">{t('Paste it before the closing body tag of the site.')}</p>
{/* The box of the script takes focus, so a keyboard scrolls its long lines (axe, FRU-239). */}
<Snippet label={t('Script')}>{snippet}</Snippet>
<Button
tone="outline"
className="mt-2"
onClick={() => void copyText(snippet).then((taken) => setCopied(taken ? 'yes' : 'refused'))}
>
<Button tone="outline" className="mt-2" onClick={copy}>
{copied === 'yes' ? t('Copied') : t('Copy the script')}
</Button>
{copied === 'refused' ? (
Expand All @@ -79,7 +103,7 @@ export function InstallOptions({ workspace, site }: { workspace: Workspace; site
'Nothing to change on the site. Connect this browser, then click the Fruitback icon on the page that opens.',
)}
</p>
<Button tone="outline" onClick={() => void connect()}>
<Button tone="outline" onClick={connect}>
{t('Connect this browser')}
</Button>
{problem === undefined ? null : (
Expand Down
22 changes: 21 additions & 1 deletion apps/console/app/messages.fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ export const FRENCH: Readonly<Record<string, string>> = {
'Paste the full address of the site, starting with https://.':
'Collez l’adresse complète du site, en commençant par https://.',
'Site address': 'Adresse du site',
Finish: 'Terminer',
Script: 'Script',
'Paste it before the closing body tag of the site.': 'Collez-le avant la balise body fermante du site.',
Copied: 'Copié',
Expand Down Expand Up @@ -515,4 +514,25 @@ export const FRENCH: Readonly<Record<string, string>> = {
'Open {site}': 'Ouvrir {site}',
'Using Firefox?': 'Vous utilisez Firefox ?',
'Get the Firefox add-on': 'Obtenir le module pour Firefox',
// The last step of the setup, and the example page of its preview (FRU-242).
'An example page. Pins show the same way on your site.':
'Une page d’exemple. Les pins s’affichent de la même façon sur votre site.',
'Leave feedback': 'Laisser un feedback',
'Click Leave feedback, then any part of the page.':
'Cliquez sur Laisser un feedback, puis sur n’importe quelle partie de la page.',
'Use one, or both.': 'Utilisez l’un, ou les deux.',
'Browser extension': 'Extension de navigateur',
"Nothing to deploy. Right for a client's site you cannot touch.":
'Rien à déployer. Convient au site d’un client auquel vous ne pouvez pas toucher.',
'Add it, then connect this browser: a page opens, and you click the Fruitback icon there.':
'Ajoutez-la, puis connectez ce navigateur : une page s’ouvre, et vous y cliquez sur l’icône Fruitback.',
'Script tag': 'Balise de script',
'Before the closing body tag. Members see their pins on every visit.':
'Avant la balise body fermante. Les membres voient leurs pins à chaque visite.',
'Before the closing body tag. Every visitor sees the pins.':
'Avant la balise body fermante. Chaque visiteur voit les pins.',
'Copy script': 'Copier le script',
'Open the site, click Leave feedback, then any part of the page.':
'Ouvrez le site, cliquez sur Laisser un feedback, puis sur n’importe quelle partie de la page.',
'Open the console': 'Ouvrir la console',
};
190 changes: 190 additions & 0 deletions apps/console/app/preview-scene.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
The example page of the setup (`preview-scene.tsx`, FRU-242): a shop that does not exist, drawn in
the window of the preview.

WARNING: the colours and the serif here are the example site's, not Fruitback's. They are no tokens,
and nothing of the console takes them. What is Fruitback's on this picture (the two buttons of the
widget, the hint) is drawn with the tokens of `app.css`.
*/
.scene {
position: relative;
height: 100%;
background: #f6f6f3;
color: #22302a;
font-family: Georgia, 'Times New Roman', serif;
/* A picture: nothing in it is selected or pressed. */
user-select: none;
}

.scene__nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 28px;
line-height: normal;
}

.scene__name {
font-size: 17px;
font-weight: 700;
letter-spacing: 0.02em;
}

.scene__links {
display: flex;
gap: 20px;
font-family: var(--font-sans);
font-size: 13px;
}

.scene__hero {
position: relative;
margin: 0 28px;
padding: 44px 36px 40px;
border-radius: 6px;
background: #2f4a3a;
color: #f6f6f3;
}

.scene__headline {
position: relative;
max-width: 340px;
font-size: 40px;
line-height: 1.06;
}

.scene__tagline {
margin: 14px 0 22px;
color: #dfe6df;
font-family: var(--font-sans);
font-size: 14px;
line-height: normal;
}

.scene__action {
display: inline-block;
padding: 10px 16px;
border-radius: 4px;
background: #f6f6f3;
color: #22302a;
font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
line-height: normal;
}

.scene__products {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 16px;
padding: 24px 28px;
}

.scene__photo {
display: flex;
align-items: flex-end;
box-sizing: border-box;
height: 120px;
padding: 8px;
border-radius: 4px;
font-family: var(--font-sans);
font-size: 11px;
line-height: normal;
}

.scene__photo[data-tone='blue'] {
background: #d8dee8;
color: #3f4652;
}

.scene__photo[data-tone='green'] {
background: #cfe0d6;
color: #35483c;
}

.scene__photo[data-tone='rose'] {
background: #ecd5cf;
color: #5c3a36;
}

.scene__product {
margin-top: 8px;
font-size: 14px;
line-height: normal;
}

/* What is Fruitback's on the picture: the hint, and the two buttons of the widget. */
.scene__hint {
position: absolute;
right: 18px;
bottom: 74px;
max-width: 230px;
padding: 10px 14px;
border-radius: var(--radius-box);
background: var(--color-ink);
color: var(--surface);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
font-family: var(--font-sans);
font-size: var(--text-hint);
line-height: 1.45;
}

.scene__hint::after {
position: absolute;
right: 52px;
bottom: -5px;
width: 10px;
height: 10px;
background: var(--color-ink);
transform: rotate(45deg);
content: '';
}

.scene__dock {
position: absolute;
right: 18px;
bottom: 18px;
display: flex;
align-items: center;
gap: 8px;
font-family: var(--font-sans);
}

.scene__settings {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--color-avatar);
color: var(--surface);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25);
}

.scene__settings svg {
width: 16px;
height: 16px;
}

.scene__launch {
display: flex;
align-items: center;
gap: 8px;
height: 40px;
padding: 0 16px;
border-radius: 999px;
background: var(--accent);
color: var(--accent-foreground);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25);
font-size: var(--text-hint);
font-weight: 600;
line-height: normal;
letter-spacing: var(--tracking-button);
}

.scene__launch svg {
width: 12px;
height: 14px;
}
72 changes: 72 additions & 0 deletions apps/console/app/preview-scene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import './preview-scene.css';

/**
* The example page of the setup (FRU-242): the scene of screen 01 of the mockup of 2026-10-13, in
* the window of the preview.
*
* It is a picture, not the site of the person: nothing here asks their site for anything, and no
* frame loads it (decided on 2026-10-13: a fixed illustration). So it is hidden from a screen
* reader and `inert`: its two buttons are drawn, and take no focus and no press. The caption under
* the window says what the picture is.
*
* The words of the shop are the picture's, in English as on the mockup. The two sentences that are
* Fruitback's come from the screen, in the language of the person.
*/
export function PreviewScene({ launch, hint }: { launch: string; hint: string }) {
return (
<div className="scene" aria-hidden="true" inert>
<div className="scene__nav">
<span className="scene__name">Acme Outdoor</span>
<span className="scene__links">
<span>Shop</span>
<span>Journal</span>
<span>Stores</span>
</span>
</div>
<div className="scene__hero">
<p className="scene__headline">Pack light, walk far.</p>
<p className="scene__tagline">The spring range is in the shops.</p>
<span className="scene__action">Shop the range</span>
</div>
<div className="scene__products">
<div>
<div className="scene__photo" data-tone="blue">
[product photo]
</div>
<p className="scene__product">Ridge 28 pack</p>
</div>
<div>
<div className="scene__photo" data-tone="green">
[product photo]
</div>
<p className="scene__product">Fell shell jacket</p>
</div>
<div>
<div className="scene__photo" data-tone="rose">
[product photo]
</div>
<p className="scene__product">Camp mug, enamel</p>
</div>
</div>

<div className="scene__hint">{hint}</div>
<div className="scene__dock">
<span className="scene__settings">
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
fillRule="evenodd"
d="M19.62 10.33 L22.10 10.60 L22.10 13.40 L19.62 13.67 L18.57 16.21 L20.13 18.16 L18.16 20.13 L16.21 18.57 L13.67 19.62 L13.40 22.10 L10.60 22.10 L10.33 19.62 L7.79 18.57 L5.84 20.13 L3.87 18.16 L5.43 16.21 L4.38 13.67 L1.90 13.40 L1.90 10.60 L4.38 10.33 L5.43 7.79 L3.87 5.84 L5.84 3.87 L7.79 5.43 L10.33 4.38 L10.60 1.90 L13.40 1.90 L13.67 4.38 L16.21 5.43 L18.16 3.87 L20.13 5.84 L18.57 7.79 Z M15.10 12 A3.1 3.1 0 1 0 8.90 12 A3.1 3.1 0 1 0 15.10 12 Z"
/>
</svg>
</span>
<span className="scene__launch">
<svg viewBox="16 6 96 116">
<path d="M 64 121.9 L 30.1 87.9 A 48 48 0 1 1 97.9 87.9 Z" fill="currentColor" />
</svg>
{launch}
</span>
</div>
</div>
);
}
Loading
Loading