NobGit
public imalexnord read

Slot

Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 79% CSS 19% SQL 2% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git

Commit

Keep booking flow in one panel

83e0c31
Alex Nord <[email protected]> 3 days ago
src/App.tsx    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/styles.css | 29 ++++++++++++++---------------
 2 files changed, 65 insertions(+), 15 deletions(-)

Diff

diff --git a/src/App.tsx b/src/App.tsx
index c829779..d86bf48 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -414,6 +414,57 @@ function PublicBooking() {
                   </div>
                 </div>
               )}
+
+              {selectedSlot && duration && (
+                <div className="inline-details">
+                  <div className="drawer-summary">
+                    <p className="step-label">04 · YOUR DETAILS</p>
+                    <h2>{selectedSlot.time} · {duration} minutes</h2>
+                    <p>{prettyDate(selectedDate, config.timezone)}</p>
+                  </div>
+                  <form className="details-form" onSubmit={submitBooking}>
+                    <div className="form-row">
+                      <label>Name<input name="name" required autoComplete="name" placeholder="Your name" /></label>
+                      <label>Email<input name="email" type="email" required autoComplete="email" placeholder="[email protected]" /></label>
+                    </div>
+
+                    {config.allowMeetingModes.length > 1 && (
+                      <div className="mode-picker">
+                        <span className="field-label">Meeting</span>
+                        <div className="mode-options">
+                          {config.allowMeetingModes.map((mode) => {
+                            const Icon = modeMeta[mode].icon;
+                            return (
+                              <button
+                                key={mode}
+                                type="button"
+                                className={`mode-option ${meetingMode === mode ? 'active' : ''}`}
+                                onClick={() => setMeetingMode(mode)}
+                              >
+                                <Icon size={18} />
+                                <span><strong>{modeMeta[mode].label}</strong><small>{modeMeta[mode].help}</small></span>
+                              </button>
+                            );
+                          })}
+                        </div>
+                      </div>
+                    )}
+
+                    {meetingMode === 'phone' && (
+                      <label>Phone number<input name="phone" required autoComplete="tel" placeholder="+46 ..." /></label>
+                    )}
+                    <label>Anything I should know? <span className="optional">Optional</span>
+                      <textarea name="message" rows={3} placeholder="Context, topic, links..." />
+                    </label>
+                    <label className="honeypot" aria-hidden="true">Website<input name="website" tabIndex={-1} autoComplete="off" /></label>
+
+                    {error && <div className="error-banner">{error}</div>}
+                    <button className="primary-button" type="submit" disabled={booking}>
+                      {booking ? <><Loader2 className="spin" size={18} /> Booking...</> : <>Book this time <ArrowRight size={18} /></>}
+                    </button>
+                  </form>
+                </div>
+              )}
             </>
           )}
         </aside>
diff --git a/src/styles.css b/src/styles.css
index eb20172..d6b5fb2 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -385,7 +385,12 @@ a { color: inherit; text-decoration: none; }
 .day:disabled { cursor: default; opacity: .25; }
 .day.outside:disabled { opacity: .12; }
 
-.selection-panel { padding: 38px 34px; background: color-mix(in srgb, var(--surface-soft) 52%, var(--surface)); }
+.selection-panel {
+  padding: 38px 34px;
+  background: color-mix(in srgb, var(--surface-soft) 52%, var(--surface));
+  overflow-y: auto;
+  min-height: 0;
+}
 .selection-heading { margin-bottom: 25px; }
 .selection-heading h2 { font-size: 19px; }
 .empty-state {
@@ -434,18 +439,11 @@ a { color: inherit; text-decoration: none; }
 .duration-button.active { background: var(--text); color: var(--bg); border-color: var(--text); }
 .duration-button.active span { color: color-mix(in srgb, var(--bg) 65%, transparent); }
 
-.details-drawer {
-  width: 100%;
-  max-width: 1320px;
-  margin: 0;
-  padding: 26px 28px;
-  border: 1px solid var(--border);
-  border-radius: 22px;
-  background: var(--surface);
-  box-shadow: var(--shadow);
-  display: grid;
-  grid-template-columns: .55fr 1.45fr;
-  gap: 44px;
+.details-drawer { display: none; }
+.inline-details {
+  border-top: 1px solid var(--border);
+  margin-top: 28px;
+  padding-top: 24px;
   animation: drawer-in 180ms ease-out;
 }
 @keyframes drawer-in { from { opacity: 0; transform: translateY(10px); } }
@@ -523,10 +521,10 @@ input:focus, textarea:focus, select:focus { border-color: color-mix(in srgb, var
 .success-banner { color: var(--success); background: color-mix(in srgb, var(--success) 8%, transparent); border: 1px solid color-mix(in srgb, var(--success) 22%, transparent); margin-bottom: 14px; }
 .floating-error { width: 100%; max-width: 1320px; margin: 0; }
 .booking-page footer {
-  position: absolute;
+  position: fixed;
   left: 26px;
   right: 26px;
-  bottom: 28px;
+  bottom: 10px;
   max-width: 1320px;
   width: calc(100% - 52px);
   margin: 0;
@@ -536,6 +534,7 @@ input:focus, textarea:focus, select:focus { border-color: color-mix(in srgb, var
   font-size: 10px;
   color: var(--faint);
   pointer-events: none;
+  z-index: 20;
 }
 .booking-page footer a { pointer-events: auto; }
 .powered-by {