feat(brand): brand assets at Shopify sizes; public landing, privacy and terms
- brand/: 1200x1200 full-bleed app icon, 1600x900 feature image, transparent logos, favicons, guidelines (palette, type, usage) and the build script - / is now a branded landing page with no shop-domain form (App Store 2.3.1) - /privacy and /terms describe Datewise's actual data handling and sub-processors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -7,6 +7,8 @@ export default function App() {
|
|||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://cdn.shopify.com/" />
|
<link rel="preconnect" href="https://cdn.shopify.com/" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon-180.png" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import type { LoaderFunctionArgs } from "react-router";
|
|
||||||
import { redirect } from "react-router";
|
|
||||||
|
|
||||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
if (url.searchParams.get("shop")) throw redirect(`/app?${url.searchParams.toString()}`);
|
|
||||||
throw redirect("/auth/login");
|
|
||||||
};
|
|
||||||
64
app/routes/_index/route.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
|
||||||
|
import { redirect } from "react-router";
|
||||||
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => [
|
||||||
|
{ title: "Datewise: Delivery & Pickup scheduling for Shopify" },
|
||||||
|
{ name: "description", content: "Let customers choose store pickup, local delivery or shipping, then a date and time, right in the Shopify cart." },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.searchParams.get("shop")) throw redirect(`/app?${url.searchParams.toString()}`);
|
||||||
|
// App Store requirement 2.3.1: never ask merchants to type their shop domain.
|
||||||
|
// Installs start from the Shopify App Store; this is a public landing page only.
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTACT = "info@metatroncubesolutions.com";
|
||||||
|
|
||||||
|
export default function Landing() {
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<img src="/datewise-logo.png" alt="Datewise" className={styles.logo} width={180} height={39} />
|
||||||
|
</header>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<section className={styles.copy}>
|
||||||
|
<h1 className={styles.h1}>Let customers choose when their order arrives.</h1>
|
||||||
|
<p className={styles.lead}>
|
||||||
|
Datewise adds a delivery and pickup scheduler to your Shopify cart. Customers pick store pickup, local delivery or
|
||||||
|
shipping, then a date and time. You set the hours, cutoffs, capacity and fees.
|
||||||
|
</p>
|
||||||
|
<ul className={styles.points}>
|
||||||
|
<li>
|
||||||
|
<strong>Every slot has a limit.</strong> A chosen time is held while the customer checks out, so two customers
|
||||||
|
can't take the last place.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Ready for rush days.</strong> Festivals and holidays get their own limits, earlier cutoffs and a waitlist.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Checked at checkout, on every plan.</strong> Orders need a valid time, including Buy it now and express
|
||||||
|
checkouts.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Fewer “can I change my time?” emails.</strong> Customers reschedule from their order page, and get
|
||||||
|
reminders by email, SMS or WhatsApp.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className={styles.install}>Install Datewise from the Shopify App Store.</p>
|
||||||
|
</section>
|
||||||
|
<figure className={styles.preview}>
|
||||||
|
<img src="/datewise-widget.png" alt="The Datewise scheduler in a Shopify cart: delivery method, calendar, time slots and the held booking" width={488} height={884} />
|
||||||
|
</figure>
|
||||||
|
</main>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<a href="/privacy">Privacy policy</a>
|
||||||
|
<a href="/terms">Terms of service</a>
|
||||||
|
<a href={`mailto:${CONTACT}`}>{CONTACT}</a>
|
||||||
|
<span>© Metatroncube Software Solutions</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
app/routes/_index/styles.module.css
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/* Public landing page. Brand tokens from brand/README.md. */
|
||||||
|
.page {
|
||||||
|
--teal: #0d6b5d;
|
||||||
|
--teal-deep: #045a51;
|
||||||
|
--amber: #d98a0b;
|
||||||
|
--sage: #e8f2ed;
|
||||||
|
--ink: #1f2937;
|
||||||
|
--paper: #fafbf9;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font: 17px/1.55 Inter, "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
.header { padding: 28px clamp(16px, 5vw, 64px); }
|
||||||
|
.logo { display: block; height: 39px; width: auto; }
|
||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||||
|
gap: clamp(32px, 6vw, 96px);
|
||||||
|
align-items: center;
|
||||||
|
padding: 24px clamp(16px, 5vw, 64px) 64px;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
.copy { max-width: 36em; }
|
||||||
|
.h1 {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: clamp(34px, 5vw, 54px);
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
font-weight: 750;
|
||||||
|
color: var(--teal-deep);
|
||||||
|
}
|
||||||
|
.lead { margin: 0 0 28px; font-size: 1.1em; color: #374151; }
|
||||||
|
.points { list-style: none; margin: 0 0 32px; padding: 0; display: grid; gap: 14px; }
|
||||||
|
.points li { position: relative; padding-left: 26px; }
|
||||||
|
.points li::before {
|
||||||
|
content: ""; position: absolute; left: 4px; top: 0.5em; width: 8px; height: 8px;
|
||||||
|
border-radius: 50%; background: var(--teal);
|
||||||
|
}
|
||||||
|
.points strong { color: var(--ink); }
|
||||||
|
.install {
|
||||||
|
display: inline-block; margin: 0; padding: 12px 18px; border-radius: 10px;
|
||||||
|
background: var(--sage); color: var(--teal-deep); font-weight: 600;
|
||||||
|
}
|
||||||
|
.preview { margin: 0; min-width: 0; width: 100%; display: flex; justify-content: center; }
|
||||||
|
.preview img {
|
||||||
|
display: block; width: 100%; max-width: 420px; height: auto; box-sizing: border-box; border-radius: 18px; background: #fff;
|
||||||
|
padding: 14px; border: 1px solid rgba(13, 107, 93, 0.14);
|
||||||
|
box-shadow: 0 24px 60px -24px rgba(4, 90, 81, 0.35);
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 8px 24px; padding: 24px clamp(16px, 5vw, 64px);
|
||||||
|
border-top: 1px solid rgba(31, 41, 55, 0.1); font-size: 14px; color: #6b7280;
|
||||||
|
}
|
||||||
|
.footer a { color: var(--teal); }
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.main { grid-template-columns: 1fr; }
|
||||||
|
.preview img { max-width: 360px; }
|
||||||
|
}
|
||||||
63
app/routes/privacy.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Public privacy policy (required for the App Store listing): https://<app domain>/privacy
|
||||||
|
// Keep every statement true to the code: update this page whenever data handling changes.
|
||||||
|
import type { MetaFunction } from "react-router";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => [{ title: "Privacy policy · Datewise" }];
|
||||||
|
|
||||||
|
const CONTACT = "info@metatroncubesolutions.com";
|
||||||
|
|
||||||
|
export default function Privacy() {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "48px auto", padding: "0 20px", font: "16px/1.6 Inter, 'Segoe UI', system-ui, sans-serif", color: "#1f2937" }}>
|
||||||
|
<p><a href="/">Datewise</a></p>
|
||||||
|
<h1>Privacy policy</h1>
|
||||||
|
<p>
|
||||||
|
Last updated: October 14, 2026. This policy explains what the Datewise app (by Metatroncube Software Solutions) collects
|
||||||
|
when a merchant installs it on a Shopify store, and when that store's customers choose a delivery or pickup time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>What we collect</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Store information:</strong> shop domain, currency, time zone, locations and the plan you choose.</li>
|
||||||
|
<li><strong>Your settings:</strong> opening hours, time slots, capacity, cutoffs, holidays, product rules, delivery zones and fees.</li>
|
||||||
|
<li>
|
||||||
|
<strong>Bookings:</strong> for each scheduled order, the order ID and number, the chosen method, location, date and time,
|
||||||
|
the items, and the customer's name, email, phone number and delivery address, taken from the Shopify order.
|
||||||
|
</li>
|
||||||
|
<li><strong>Waitlist entries:</strong> the email address or phone number a customer gives to join a waitlist for a full date.</li>
|
||||||
|
<li><strong>Postal codes</strong> a customer enters to check delivery, used only to find their delivery zone.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>How we use it</h2>
|
||||||
|
<p>
|
||||||
|
Only to provide the app: show available times, hold and record bookings, show bookings and driver run sheets to you, send the
|
||||||
|
reminders and waitlist messages you switch on, and let customers change their own booking. We don't sell data or use it for
|
||||||
|
advertising, profiling or training AI models.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Who processes it</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Our hosting provider, for servers and the database.</li>
|
||||||
|
<li>Resend, to send reminder and waitlist emails.</li>
|
||||||
|
<li>Twilio and Meta (WhatsApp), only if you connect your own accounts for SMS or WhatsApp reminders.</li>
|
||||||
|
<li>Google Maps, to turn delivery postal codes and addresses into distances, if you use distance-based delivery zones.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Security</h2>
|
||||||
|
<p>
|
||||||
|
Data is encrypted in transit (HTTPS). Messaging credentials you enter are encrypted at rest. Access to our systems is limited
|
||||||
|
to authorised staff. Our data processing terms are in the <a href="/terms">terms of service</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Retention and deletion</h2>
|
||||||
|
<p>
|
||||||
|
When a customer asks Shopify to erase their data, we remove their name, email, phone and address from bookings and delete
|
||||||
|
their waitlist entries. When you uninstall the app, pending reminders stop at once, and we delete all data about your store
|
||||||
|
48 hours later, when Shopify sends the shop redaction request.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>Questions or requests: <a href={`mailto:${CONTACT}`}>{CONTACT}</a></p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
app/routes/terms.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Public terms of service with data processing terms (the merchant agreement Shopify's protected customer
|
||||||
|
// data requirements ask for): https://<app domain>/terms
|
||||||
|
import type { MetaFunction } from "react-router";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => [{ title: "Terms of service · Datewise" }];
|
||||||
|
|
||||||
|
const CONTACT = "info@metatroncubesolutions.com";
|
||||||
|
|
||||||
|
export default function Terms() {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "48px auto", padding: "0 20px", font: "16px/1.6 Inter, 'Segoe UI', system-ui, sans-serif", color: "#1f2937" }}>
|
||||||
|
<p><a href="/">Datewise</a></p>
|
||||||
|
<h1>Terms of service</h1>
|
||||||
|
<p>
|
||||||
|
Last updated: October 14, 2026. These terms apply when a merchant installs Datewise (provided by Metatroncube Software
|
||||||
|
Solutions, “we”) on a Shopify store. By installing the app you agree to them. Our <a href="/privacy">privacy policy</a> forms
|
||||||
|
part of these terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>The service</h2>
|
||||||
|
<p>
|
||||||
|
Datewise lets your customers choose store pickup, local delivery or shipping and a date and time, records those bookings,
|
||||||
|
and helps you run them. Plans and prices are shown and billed through Shopify. You can change plans or uninstall at any time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Your responsibilities</h2>
|
||||||
|
<p>
|
||||||
|
You set your hours, capacity, cutoffs, fees and delivery zones, and you're responsible for fulfilling orders at the times
|
||||||
|
your customers choose. Check your settings before busy periods. Messages sent through your own Twilio or WhatsApp accounts
|
||||||
|
are subject to those providers' terms and charges.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Data processing terms</h2>
|
||||||
|
<p>For personal data about your customers, you are the controller and we act as your processor. We:</p>
|
||||||
|
<ul>
|
||||||
|
<li>process customer data (name, email, phone, delivery address, order and booking details) only to provide the app's features and on your instructions given through the app;</li>
|
||||||
|
<li>do not sell or share personal data, and do not use it for advertising, profiling, automated decisions or training AI models;</li>
|
||||||
|
<li>encrypt data in transit (HTTPS) and encrypt messaging credentials at rest;</li>
|
||||||
|
<li>limit access to our systems to authorised staff bound by confidentiality;</li>
|
||||||
|
<li>use these sub-processors: our hosting provider, Resend (email) and Google Maps (delivery distances); Twilio and Meta only when you connect your own accounts. We will tell you before adding others;</li>
|
||||||
|
<li>notify you without undue delay, and within 72 hours, after becoming aware of a breach affecting your data;</li>
|
||||||
|
<li>help you respond to customer requests (access, deletion) received through Shopify's privacy webhooks;</li>
|
||||||
|
<li>delete all data about your store 48 hours after you uninstall the app.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
You are responsible for telling your customers how their data is used in your own store's privacy notice, for having a
|
||||||
|
lawful basis for the processing, and for having consent to send SMS or WhatsApp reminders where the law requires it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Liability</h2>
|
||||||
|
<p>
|
||||||
|
The app is provided as is. We work to keep it available and accurate, but we can't guarantee uninterrupted service. To the
|
||||||
|
extent allowed by law, our total liability is limited to the fees you paid for the app in the 12 months before the claim.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Changes and contact</h2>
|
||||||
|
<p>We may update these terms and will notify you in the app or by email for material changes. Questions: <a href={`mailto:${CONTACT}`}>{CONTACT}</a></p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
brand/README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Datewise brand
|
||||||
|
|
||||||
|
**Name:** Datewise · **Listing name:** Datewise: Delivery & Pickup · **Tagline:** The right day, every order.
|
||||||
|
**Personality:** calm, reliable, organised, quietly clever, friendly.
|
||||||
|
Full board: `source/brand-board.png`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Size | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| `shopify/app-icon-1200x1200.png` | 1200×1200 | **Partner Dashboard → App icon** and App Store listing icon. Full bleed, no text; Shopify rounds the corners. |
|
||||||
|
| `shopify/feature-image-1600x900.png` | 1600×900 | **App Store listing → Feature image.** No pricing, reviews or outcome claims. |
|
||||||
|
| `logo/datewise-logo.png` | 2104×460, transparent | Primary horizontal logo on light backgrounds |
|
||||||
|
| `logo/datewise-logo-white.png` | transparent | Logo on teal or dark backgrounds |
|
||||||
|
| `logo/datewise-logo-mono.png` | transparent | Single-colour use (charcoal) |
|
||||||
|
| `logo/datewise-symbol.png` | 894×894, transparent | Symbol alone (avatars, social) |
|
||||||
|
| `web/favicon.ico`, `web/favicon-32.png`, `web/apple-touch-icon-180.png`, `web/icon-512.png` | | Website icons (copies live in `public/`) |
|
||||||
|
| `source/*` | | The original generated artwork, unchanged |
|
||||||
|
|
||||||
|
Screenshots for the listing (1600×900, 3–6 with alt text) come later, from the dev store (launch Phase 8).
|
||||||
|
|
||||||
|
Rebuild everything from the sources: `python -I brand/build_brand.py <sources dir> <widget screenshot> brand public`.
|
||||||
|
The originals are raster images generated by AI. Before printing or large-format use, have the symbol and wordmark redrawn as SVG.
|
||||||
|
|
||||||
|
## Colour
|
||||||
|
|
||||||
|
| Token | Hex | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| Primary teal | `#0D6B5D` | Brand colour, primary buttons, selected states. Default accent of the storefront widget. |
|
||||||
|
| Deep teal | `#045A51` | Icon and feature-image background, headings on light pages |
|
||||||
|
| Amber | `#D98A0B` | Highlights only: the symbol's rays, busy and festival days. Never for body text. |
|
||||||
|
| Sage tint | `#E8F2ED` | Calm surfaces, confirmation backgrounds |
|
||||||
|
| Charcoal | `#1F2937` | Text |
|
||||||
|
| Off-white | `#FAFBF9` | Page background |
|
||||||
|
|
||||||
|
White on `#0D6B5D` has a contrast ratio of about 6.4:1 (passes WCAG AA for text). Amber on white is about 2.8:1 and fails for text, so use it only for marks and dots.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
**Inter** (Google Fonts). Headings 700–750 with slightly tight tracking (−0.02em); body 400–450.
|
||||||
|
The admin uses Polaris's own type (Built for Shopify), and the storefront widget inherits the merchant's theme font.
|
||||||
|
|
||||||
|
## Logo use
|
||||||
|
|
||||||
|
- Clear space: at least the height of the calendar's ring tabs on every side.
|
||||||
|
- Minimum size: symbol 16px (favicon), horizontal logo 96px wide.
|
||||||
|
- Don't recolour the rays to anything but amber or white, stretch the logo, add effects, or place the colour logo on busy photos.
|
||||||
|
- The symbol's motifs echo the product UI: the calendar is the booking, the notched edges are the ticket stub (the cart confirmation), and the rays mark the confirmed moment.
|
||||||
|
|
||||||
|
## Where the brand appears in the app
|
||||||
|
|
||||||
|
- Public landing page `/`, `/privacy` and `/terms`: logo, palette, Inter.
|
||||||
|
- Storefront widget: accent `#0D6B5D` by default (merchants can change it in Settings).
|
||||||
|
- Admin: Polaris only. Branding there is the app icon and name, set in the Partner Dashboard.
|
||||||
138
brand/build_brand.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
"""Build Datewise brand assets (Shopify sizes) from the generated source images.
|
||||||
|
usage: python -I build_brand.py <images_dir> <widget_screenshot> <out_brand_dir> <out_public_dir>
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||||
|
|
||||||
|
SRC, SHOT, OUT, PUB = sys.argv[1:5]
|
||||||
|
TEAL = (13, 107, 93) # #0D6B5D
|
||||||
|
TEAL_DEEP = (4, 90, 81) # background of the reversed logo
|
||||||
|
SAGE = (232, 242, 237) # #E8F2ED
|
||||||
|
OFF_WHITE = (250, 251, 249) # #FAFBF9
|
||||||
|
FONT = os.path.expanduser(r"~/AppData/Local/Microsoft/Windows/Fonts/Inter-VariableFont_slnt,wght.ttf")
|
||||||
|
|
||||||
|
for d in ("source", "logo", "shopify", "web"):
|
||||||
|
os.makedirs(os.path.join(OUT, d), exist_ok=True)
|
||||||
|
os.makedirs(PUB, exist_ok=True)
|
||||||
|
src = lambda n: Image.open(os.path.join(SRC, f"{n}.png"))
|
||||||
|
out = lambda *p: os.path.join(OUT, *p)
|
||||||
|
|
||||||
|
# ---- originals, renamed -------------------------------------------------------------
|
||||||
|
for n, name in {1: "brand-board", 2: "app-icon-rounded-preview", 3: "logo-reversed-on-teal", 4: "logo-mono", 5: "symbol", 6: "logo"}.items():
|
||||||
|
shutil.copy(os.path.join(SRC, f"{n}.png"), out("source", f"{name}.png"))
|
||||||
|
|
||||||
|
|
||||||
|
def trim(im, pad=0):
|
||||||
|
box = im.getchannel("A").point(lambda v: 255 if v > 8 else 0).getbbox()
|
||||||
|
im = im.crop(box)
|
||||||
|
if pad:
|
||||||
|
c = Image.new("RGBA", (im.width + 2 * pad, im.height + 2 * pad), (0, 0, 0, 0))
|
||||||
|
c.paste(im, (pad, pad))
|
||||||
|
im = c
|
||||||
|
return im
|
||||||
|
|
||||||
|
|
||||||
|
# ---- transparent logos ----------------------------------------------------------------
|
||||||
|
logo = trim(src(6).convert("RGBA"), 24)
|
||||||
|
logo.save(out("logo", "datewise-logo.png"))
|
||||||
|
trim(src(4).convert("RGBA"), 24).save(out("logo", "datewise-logo-mono.png"))
|
||||||
|
symbol = trim(src(5).convert("RGBA"))
|
||||||
|
side = max(symbol.size) + 48
|
||||||
|
sq = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
||||||
|
sq.paste(symbol, ((side - symbol.width) // 2, (side - symbol.height) // 2))
|
||||||
|
sq.save(out("logo", "datewise-symbol.png"))
|
||||||
|
|
||||||
|
# White logo for dark backgrounds: alpha from distance to the teal background, colour from the source.
|
||||||
|
rev = np.asarray(src(3).convert("RGB")).astype(np.float32)
|
||||||
|
bg = np.array(TEAL_DEEP, np.float32)
|
||||||
|
dist = np.sqrt(((rev - bg) ** 2).sum(-1))
|
||||||
|
alpha = np.clip(dist / np.sqrt(((np.array([255, 255, 255]) - bg) ** 2).sum()) * 255 * 1.15, 0, 255).astype(np.uint8)
|
||||||
|
white = np.dstack([np.full(alpha.shape, 255, np.uint8)] * 3 + [alpha])
|
||||||
|
trim(Image.fromarray(white, "RGBA"), 24).save(out("logo", "datewise-logo-white.png"))
|
||||||
|
|
||||||
|
# ---- full-bleed app icon (1200x1200, no rounded corners, no padding) ------------------
|
||||||
|
icon = src(2).convert("RGB")
|
||||||
|
a = np.asarray(icon).astype(np.float32)
|
||||||
|
x0, y0, x1, y1 = 185, 185, 1070, 1070 # the rounded square
|
||||||
|
crop = a[y0:y1, x0:x1]
|
||||||
|
h, w, _ = crop.shape
|
||||||
|
yy, xx = np.mgrid[0:h, 0:w]
|
||||||
|
# Fit the teal gradient (quadratic in x, y) on pixels that are clearly teal background.
|
||||||
|
teal_mask = (crop[..., 1] > 60) & (crop[..., 1] < 125) & (crop[..., 0] < 25) & (abs(crop[..., 1] - crop[..., 2]) < 20)
|
||||||
|
content = np.zeros((h, w), bool)
|
||||||
|
content[340 - y0:885 - y0, 330 - x0:1005 - x0] = True
|
||||||
|
fit = teal_mask & ~content
|
||||||
|
X = np.stack([np.ones_like(xx), xx, yy, xx * xx, yy * yy, xx * yy], -1).astype(np.float64) / np.array([1, w, h, w * w, h * h, w * h])
|
||||||
|
coef = [np.linalg.lstsq(X[fit], crop[..., c][fit], rcond=None)[0] for c in range(3)]
|
||||||
|
model = np.stack([X @ coef[c] for c in range(3)], -1)
|
||||||
|
# Keep the original artwork only inside the content box; everything else is the smooth gradient.
|
||||||
|
dev = np.sqrt(((crop - model) ** 2).sum(-1))
|
||||||
|
m = np.where(content, np.clip((dev - 6) / 18, 0, 1), 0).astype(np.float32)
|
||||||
|
m = np.asarray(Image.fromarray((m * 255).astype(np.uint8)).filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(1))).astype(np.float32) / 255
|
||||||
|
full = model * (1 - m[..., None]) + crop * m[..., None]
|
||||||
|
app_icon = Image.fromarray(np.clip(full, 0, 255).astype(np.uint8), "RGB").resize((1200, 1200), Image.LANCZOS)
|
||||||
|
app_icon.save(out("shopify", "app-icon-1200x1200.png"), optimize=True)
|
||||||
|
|
||||||
|
# ---- web icons ---------------------------------------------------------------------------
|
||||||
|
app_icon.resize((180, 180), Image.LANCZOS).save(out("web", "apple-touch-icon-180.png"))
|
||||||
|
app_icon.resize((512, 512), Image.LANCZOS).save(out("web", "icon-512.png"))
|
||||||
|
fav = app_icon.resize((256, 256), Image.LANCZOS)
|
||||||
|
fav.save(out("web", "favicon.ico"), sizes=[(16, 16), (32, 32), (48, 48)])
|
||||||
|
app_icon.resize((32, 32), Image.LANCZOS).save(out("web", "favicon-32.png"))
|
||||||
|
for f in ("favicon.ico", "apple-touch-icon-180.png"):
|
||||||
|
shutil.copy(out("web", f), os.path.join(PUB, f))
|
||||||
|
logo.save(os.path.join(PUB, "datewise-logo.png"))
|
||||||
|
|
||||||
|
# ---- feature image (1600x900): logo + one line + the real cart widget ------------------
|
||||||
|
def font(size, weight):
|
||||||
|
f = ImageFont.truetype(FONT, size)
|
||||||
|
try:
|
||||||
|
f.set_variation_by_axes([weight, 0] if len(f.get_variation_axes()) == 2 else [weight])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return f
|
||||||
|
|
||||||
|
W, H = 1600, 900
|
||||||
|
feat = Image.new("RGB", (W, H), TEAL_DEEP)
|
||||||
|
d = ImageDraw.Draw(feat)
|
||||||
|
# Soft lighter wash top-left, echoing the icon's gradient.
|
||||||
|
wash = Image.new("L", (W, H), 0)
|
||||||
|
ImageDraw.Draw(wash).ellipse((-500, -600, 1000, 700), fill=70)
|
||||||
|
wash = wash.filter(ImageFilter.GaussianBlur(160))
|
||||||
|
feat = Image.composite(Image.new("RGB", (W, H), (20, 125, 110)), feat, wash)
|
||||||
|
d = ImageDraw.Draw(feat)
|
||||||
|
|
||||||
|
white_logo = Image.open(out("logo", "datewise-logo-white.png"))
|
||||||
|
lw = 560
|
||||||
|
white_logo = white_logo.resize((lw, round(white_logo.height * lw / white_logo.width)), Image.LANCZOS)
|
||||||
|
feat.paste(white_logo, (96, 300), white_logo)
|
||||||
|
d.text((120, 300 + white_logo.height + 28), "Customers choose their delivery\nor pickup time in the cart.", font=font(46, 600), fill=(255, 255, 255), spacing=14)
|
||||||
|
d.text((120, 300 + white_logo.height + 172), "Store pickup · Local delivery · Shipping", font=font(26, 450), fill=SAGE)
|
||||||
|
|
||||||
|
shot = Image.open(SHOT).convert("RGB")
|
||||||
|
panel = shot.crop((16, 228, 504, 1112)) # the scheduler panel in the e2e cart screenshot
|
||||||
|
ph = 780
|
||||||
|
panel = panel.resize((round(panel.width * ph / panel.height), ph), Image.LANCZOS)
|
||||||
|
card = Image.new("RGB", (panel.width + 48, panel.height + 48), (255, 255, 255))
|
||||||
|
card.paste(panel, (24, 24))
|
||||||
|
mask = Image.new("L", card.size, 0)
|
||||||
|
ImageDraw.Draw(mask).rounded_rectangle((0, 0, card.width - 1, card.height - 1), 28, fill=255)
|
||||||
|
cx, cy = W - card.width - 110, (H - card.height) // 2
|
||||||
|
shadow = Image.new("L", (W, H), 0)
|
||||||
|
ImageDraw.Draw(shadow).rounded_rectangle((cx + 8, cy + 24, cx + card.width + 8, cy + card.height + 24), 28, fill=110)
|
||||||
|
shadow = shadow.filter(ImageFilter.GaussianBlur(28))
|
||||||
|
feat = Image.composite(Image.new("RGB", (W, H), (2, 40, 36)), feat, shadow)
|
||||||
|
feat.paste(card, (cx, cy), mask)
|
||||||
|
feat.save(out("shopify", "feature-image-1600x900.png"), optimize=True)
|
||||||
|
|
||||||
|
for root, _, files in os.walk(OUT):
|
||||||
|
for f in sorted(files):
|
||||||
|
p = os.path.join(root, f)
|
||||||
|
try:
|
||||||
|
print(os.path.relpath(p, OUT), Image.open(p).size, f"{os.path.getsize(p) // 1024} KB")
|
||||||
|
except Exception:
|
||||||
|
print(os.path.relpath(p, OUT))
|
||||||
BIN
brand/logo/datewise-logo-mono.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
brand/logo/datewise-logo-white.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
brand/logo/datewise-logo.png
Normal file
|
After Width: | Height: | Size: 410 KiB |
BIN
brand/logo/datewise-symbol.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
brand/shopify/app-icon-1200x1200.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
brand/shopify/feature-image-1600x900.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
brand/source/app-icon-rounded-preview.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
brand/source/brand-board.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
brand/source/logo-mono.png
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
brand/source/logo-reversed-on-teal.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
brand/source/logo.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
brand/source/symbol.png
Normal file
|
After Width: | Height: | Size: 438 KiB |
BIN
brand/web/apple-touch-icon-180.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
brand/web/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
brand/web/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
brand/web/icon-512.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/apple-touch-icon-180.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/datewise-logo.png
Normal file
|
After Width: | Height: | Size: 410 KiB |
BIN
public/datewise-widget.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.9 KiB |