Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

6.3 kB Ruw bestand
import { useState, type ReactNode } from 'react';
import {
  LOCALE_LABELS,
  SUPPORTED_LOCALES,
  isLocale,
  resolveLocale,
  slugifyWorkspaceName,
} from '@wellformed/shared';
import { api } from '../api/client';
import { useI18n } from '../i18n';
import { useSession } from '../state/session';
import { useDismissSsr } from '../state/ssr';
import { useToasts } from '../state/toasts';
import { goTo, workspacePath } from '../state/route';

/**
 * Where a signed-in account lands when the URL names no workspace, or names one
 * it cannot open. Both cases are the same question — which of these do you
 * want? — so they get the same screen, with a line of explanation when the URL
 * was the reason.
 */
export function WorkspacePicker({
  unknownSlug,
  startCreating = false,
}: {
  unknownSlug?: string;
  startCreating?: boolean;
}): ReactNode {
  const session = useSession();
  const toasts = useToasts();
  const { t } = useI18n();
  useDismissSsr();

  const [creating, setCreating] = useState(startCreating || session.workspaces.length === 0);
  const [name, setName] = useState('');
  // Off by default: most people arrive with the spreadsheet they want to
  // replace, and a demo on top of real intent is furniture in the way.
  const [demoStructure, setDemoStructure] = useState(false);
  const [demoData, setDemoData] = useState(false);
  // Asked here, not guessed from a header: the language decides how every
  // number and date in the workspace is written, and someone who imports
  // "12-3-2021" should never first learn the answer from "3/12/2021".
  const [locale, setLocale] = useState(() =>
    resolveLocale(navigator.languages ?? [navigator.language]),
  );
  const [busy, setBusy] = useState(false);

  async function create() {
    setBusy(true);
    try {
      const { workspace } = await api.createWorkspace({
        name: name.trim(),
        demo: demoStructure ? (demoData ? 'full' : 'structure') : 'none',
        locale,
      });
      goTo(workspacePath(workspace.slug));
    } catch (error) {
      toasts.fail(error);
      setBusy(false);
    }
  }

  return (
    <div className="login">
      <div className="login-card wide">
        <div className="wordmark">
          Well<span>formed</span>
        </div>
        <div className="tagline">
          {session.workspaces.length ? t('picker.choose') : t('picker.first')}
        </div>

        {unknownSlug && (
          <div className="formerror" role="alert">
            <span className="mono">/w/{unknownSlug}</span>
            {t('picker.unknownAfter', session.account?.email ?? '')}
          </div>
        )}

        {session.workspaces.length > 0 && (
          <ul className="wslist">
            {session.workspaces.map((workspace) => (
              <li key={workspace.id}>
                <a href={workspacePath(workspace.slug)}>
                  <span className="wsname">{workspace.name}</span>
                  <span className="hint mono">/w/{workspace.slug}</span>
                  <span className="rolechip">{t(`role.${workspace.role}`)}</span>
                </a>
              </li>
            ))}
          </ul>
        )}

        {creating ? (
          <form
            onSubmit={(e) => {
              e.preventDefault();
              void create();
            }}
          >
            <label>
              {t('picker.workspaceName')}
              <input
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Acme Marine"
                required
                autoFocus
              />
              {name.trim() && (
                <span className="hint">
                  {t('picker.address')} <span className="mono">/w/{slugifyWorkspaceName(name)}</span>
                </span>
              )}
            </label>

            <label>
              {t('workspace.language')}
              <select
                value={locale}
                onChange={(e) => {
                  if (isLocale(e.target.value)) setLocale(e.target.value);
                }}
              >
                {SUPPORTED_LOCALES.map((loc) => (
                  <option key={loc} value={loc}>
                    {LOCALE_LABELS[loc]}
                  </option>
                ))}
              </select>
              <span className="hint">{t('workspace.languageHint')}</span>
            </label>

            {/* The demo is a property of creation, not of signup: it can only
                ever be the first thing in a workspace. */}
            <label className="checkrow">
              <input
                type="checkbox"
                checked={demoStructure}
                onChange={(e) => {
                  setDemoStructure(e.target.checked);
                  if (!e.target.checked) setDemoData(false);
                }}
              />
              <span>
                {t('picker.demoStructure')}
                <span className="hint">{t('picker.demoStructureHint')}</span>
              </span>
            </label>
            {demoStructure && (
              <label className="checkrow nested">
                <input
                  type="checkbox"
                  checked={demoData}
                  onChange={(e) => setDemoData(e.target.checked)}
                />
                <span>
                  {t('picker.demoData')}
                  <span className="hint">{t('picker.demoDataHint')}</span>
                </span>
              </label>
            )}

            <div className="rowend">
              {session.workspaces.length > 0 && (
                <button className="btn" type="button" onClick={() => setCreating(false)}>
                  {t('common.cancel')}
                </button>
              )}
              <button className="btn primary" type="submit" disabled={busy || !name.trim()}>
                {busy ? t('picker.creating') : t('picker.createIt')}
              </button>
            </div>
          </form>
        ) : (
          <button className="btn" onClick={() => setCreating(true)}>
            {t('picker.new')}
          </button>
        )}

        <p className="hint">
          {t('account.signedInAs')} {session.account?.email}.{' '}
          <button className="linkbtn" onClick={() => void session.signOut().then(() => goTo('/'))}>
            {t('chrome.signOut')}
          </button>
        </p>
      </div>
    </div>
  );
}