├── .gitignore ├── src ├── Configurator │ ├── ModalBase │ │ ├── common.less │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.less │ ├── OptionsMenu │ │ ├── pages │ │ │ ├── FieldsPreviewPage │ │ │ │ ├── Drawer │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── DrawerElement │ │ │ │ │ ├── FieldOptionsBar │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ ├── FieldOrderHandler │ │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── icons.tsx │ │ │ │ │ ├── style.less │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── FieldPropertiesDetails.tsx │ │ │ │ ├── DrawerPlaceholder │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.less │ │ │ │ ├── icons.tsx │ │ │ │ ├── DrawerJSONEditor │ │ │ │ │ └── style.less │ │ │ │ └── style.less │ │ │ ├── components │ │ │ │ ├── CapitalHeaderTitle.tsx │ │ │ │ ├── Header │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.less │ │ │ │ └── FieldPreview │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ ├── PanelsPage │ │ │ │ ├── Panel │ │ │ │ │ ├── style.less │ │ │ │ │ ├── ColorPanel │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ImagePanel │ │ │ │ │ │ ├── icons.tsx │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── FieldsPanel │ │ │ │ │ │ ├── icons.tsx │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── useContentSavingHandler.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── TextPanel │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── TabsList │ │ │ │ │ └── style.less │ │ │ ├── FieldsPropertiesEditPage │ │ │ │ ├── style.less │ │ │ │ ├── FieldPropertiesEditList │ │ │ │ │ ├── FieldPropertyPanels │ │ │ │ │ │ ├── Checkbox.tsx │ │ │ │ │ │ ├── String.tsx │ │ │ │ │ │ └── Enum.tsx │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ └── icons.tsx │ │ ├── PageContainer.tsx │ │ ├── navigation.utils │ │ │ ├── index.tsx │ │ │ ├── usePagesAmount.hook.tsx │ │ │ ├── usePageRelation.hook.tsx │ │ │ ├── navigable.hoc.tsx │ │ │ └── navigation.memory.tsx │ │ ├── style.less │ │ └── index.tsx │ ├── LanguageSelectionButton │ │ ├── index.tsx │ │ └── style.less │ ├── OptionsBar │ │ ├── index.tsx │ │ ├── style.less │ │ └── icons.tsx │ ├── Switcher │ │ ├── index.tsx │ │ └── style.less │ ├── Viewer │ │ ├── style.less │ │ └── index.tsx │ ├── style.less │ ├── RegistrationIndex.ts │ ├── TranslationsModal │ │ └── icons.tsx │ ├── CommittableTextInput.tsx │ ├── MediaModal │ │ ├── style.less │ │ ├── icons.tsx │ │ └── ModalNavigation │ │ │ └── style.less │ ├── LanguageModal │ │ └── index.tsx │ ├── staticFields.ts │ └── ExportModal │ │ ├── style.less │ │ └── index.tsx ├── Pass │ ├── layouts │ │ ├── components │ │ │ ├── EmptyField │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── Barcodes │ │ │ │ ├── empty.tsx │ │ │ │ ├── style.less │ │ │ │ ├── index.tsx │ │ │ │ └── code128.tsx │ │ │ ├── Field │ │ │ │ ├── style.less │ │ │ │ ├── FieldLabel.tsx │ │ │ │ ├── getFilteredFieldData.ts │ │ │ │ ├── fieldCommons.ts │ │ │ │ ├── index.tsx │ │ │ │ └── FieldValue.tsx │ │ │ ├── TextField │ │ │ │ ├── style.less │ │ │ │ └── index.tsx │ │ │ ├── useFallback.tsx │ │ │ ├── useClickEvent.ts │ │ │ └── ImageField │ │ │ │ └── index.tsx │ │ ├── sections │ │ │ ├── PrimaryFields │ │ │ │ ├── index.tsx │ │ │ │ ├── Strip │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Thumbnail │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ │ └── Travel │ │ │ │ │ ├── style.less │ │ │ │ │ └── index.tsx │ │ │ ├── FieldRow │ │ │ │ ├── style.less │ │ │ │ └── index.tsx │ │ │ ├── BackFields │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── Footer │ │ │ │ ├── index.tsx │ │ │ │ ├── style.less │ │ │ │ └── icons.tsx │ │ │ ├── useRegistrations.ts │ │ │ └── Header │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ ├── index.tsx │ │ ├── BoardingPass.tsx │ │ ├── Coupon.tsx │ │ ├── StoreCard.tsx │ │ ├── Generic.tsx │ │ └── EventTicket.tsx │ ├── InteractionContext.ts │ ├── useCSSCustomProperty.tsx │ └── style.less ├── public │ ├── index.tsx │ ├── index.html │ └── styles.less ├── PassSelector │ ├── SelectablePass │ │ ├── layouts │ │ │ ├── index.tsx │ │ │ ├── Coupon.tsx │ │ │ ├── StoreCard.tsx │ │ │ ├── EventTicket.tsx │ │ │ ├── Generic.tsx │ │ │ └── BoardingPass.tsx │ │ ├── useAlternativesRegistration.ts │ │ └── index.tsx │ ├── PassList.tsx │ └── index.tsx ├── store │ ├── index.ts │ ├── middlewares │ │ ├── index.ts │ │ ├── CreationMiddleware.ts │ │ └── PurgeMiddleware.ts │ ├── reducers.ts │ ├── forage.ts │ ├── projectOptions.ts │ ├── pass.ts │ └── store.ts ├── App │ ├── style.less │ └── common.less ├── model.ts ├── utils.ts ├── Loader │ ├── index.tsx │ └── style.less └── RecentSelector │ └── icons.tsx ├── vercel.json ├── .vscode └── settings.json ├── assets ├── SF-Pro-Display-Light.otf ├── SF-Pro-Display-Thin.otf ├── SF-Pro-Display-Medium.otf └── SF-Pro-Display-Regular.otf ├── partners-templates ├── helpers │ ├── isDefaultLanguage.ts │ └── hasContent.ts ├── index.json ├── index.ts └── passkit-generator.hbs ├── .prettierrc ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | assets/ 3 | .vercel 4 | dist/ 5 | -------------------------------------------------------------------------------- /src/Configurator/ModalBase/common.less: -------------------------------------------------------------------------------- 1 | @modal-background-color: #191919; 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/(.*)", "destination": "/" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[handlebars]": { 3 | "editor.formatOnSave": false 4 | }, 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /assets/SF-Pro-Display-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandercerutti/passkit-visual-designer/HEAD/assets/SF-Pro-Display-Light.otf -------------------------------------------------------------------------------- /assets/SF-Pro-Display-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandercerutti/passkit-visual-designer/HEAD/assets/SF-Pro-Display-Thin.otf -------------------------------------------------------------------------------- /assets/SF-Pro-Display-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandercerutti/passkit-visual-designer/HEAD/assets/SF-Pro-Display-Medium.otf -------------------------------------------------------------------------------- /assets/SF-Pro-Display-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexandercerutti/passkit-visual-designer/HEAD/assets/SF-Pro-Display-Regular.otf -------------------------------------------------------------------------------- /partners-templates/helpers/isDefaultLanguage.ts: -------------------------------------------------------------------------------- 1 | export default function isDefaultLanguage(value, options) { 2 | return value === "default"; 3 | } 4 | -------------------------------------------------------------------------------- /partners-templates/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "showName": "Passkit-generator (v2)", 4 | "lang": "javascript", 5 | "templateName": "passkitGenerator" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/EmptyField/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | export default function EmptyField

(props: P) { 5 | return

; 6 | } 7 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Barcodes/empty.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const EmptySquareCode = () =>
; 4 | 5 | export const EmptyBarcode = () =>
; 6 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as StripPrimaryFields } from "./Strip"; 2 | export { default as ThumbnailPrimaryFields } from "./Thumbnail"; 3 | export { default as TravelPrimaryFields } from "./Travel"; 4 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/Drawer/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../../App/common.less"; 2 | 3 | #drawer { 4 | .scrollableWithoutScrollbars(y); 5 | display: flex; 6 | flex-grow: 1; 7 | flex-direction: column; 8 | } 9 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/components/CapitalHeaderTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default function CapitalHeaderTitle({ name }: { name: string }) { 4 | const showTitle = name.replace(/([a-z])([A-Z])/g, "$1 $2"); 5 | 6 | return

{showTitle}

; 7 | } 8 | -------------------------------------------------------------------------------- /src/public/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | import * as React from "react"; 3 | import "./styles.less"; 4 | import App from "../App"; 5 | import localForage from "localforage"; 6 | 7 | localForage.config(); 8 | 9 | ReactDOM.render(, document.getElementById("root")); 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "jsxSingleQuote": false, 4 | "arrowParens": "always", 5 | "bracketSpacing": true, 6 | "endOfLine": "lf", 7 | "jsxBracketSameLine": false, 8 | "singleQuote": false, 9 | "tabWidth": 2, 10 | "trailingComma": "es5", 11 | "printWidth": 100, 12 | "semi": true 13 | } 14 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as BoardingPass } from "./BoardingPass"; 2 | export { default as Coupon } from "./Coupon"; 3 | export { default as EventTicket } from "./EventTicket"; 4 | export { default as Generic } from "./Generic"; 5 | export { default as StoreCard } from "./StoreCard"; 6 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/EmptyField/style.less: -------------------------------------------------------------------------------- 1 | .empty-field { 2 | height: 100%; // Needed for flex align-items: center on header 3 | background-color: #cecece; 4 | display: flex; 5 | flex-grow: 1; 6 | cursor: pointer; 7 | transition: background-color 0.2s ease-in-out; 8 | 9 | &:hover { 10 | background-color: #b9b9b9; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /partners-templates/helpers/hasContent.ts: -------------------------------------------------------------------------------- 1 | export default function hasContents(value, options) { 2 | if (!value) { 3 | return false; 4 | } 5 | 6 | if (value instanceof Array) { 7 | return Boolean(value.length); 8 | } 9 | 10 | if (value && typeof value === "object") { 11 | return Boolean(Object.keys(value).length); 12 | } 13 | 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Just a common container for all the pages. Nothing else. 5 | * Move along. Nothing to see here. 6 | * 7 | * @param props 8 | * @returns 9 | */ 10 | 11 | export function PageContainer(props: React.PropsWithChildren<{}>) { 12 | return
{props.children}
; 13 | } 14 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/style.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | padding: 10px; 3 | 4 | &:not(:last-child) { 5 | border-bottom: 1px solid #1c1c1c; 6 | } 7 | 8 | & h4 { 9 | margin: 0 0 10px 0; 10 | font-weight: 400; 11 | text-transform: uppercase; 12 | font-size: 14px; 13 | color: #e6e6e6; 14 | -webkit-user-select: none; 15 | user-select: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store"; /** Important here because others below depend on this */ 2 | export * as Media from "./media"; 3 | export * as Pass from "./pass"; 4 | export * as Options from "./projectOptions"; 5 | export * as Translations from "./translations"; 6 | export { default as reducers } from "./reducers"; 7 | export * as middlewares from "./middlewares"; 8 | export * as Forage from "./forage"; 9 | -------------------------------------------------------------------------------- /src/App/style.less: -------------------------------------------------------------------------------- 1 | #root .fade-enter { 2 | opacity: 0; 3 | 4 | &.fade-enter-active { 5 | opacity: 1; 6 | transition: opacity 0.3s ease-in; 7 | } 8 | } 9 | 10 | #root .fade-exit { 11 | opacity: 1; 12 | 13 | &.fade-exit-active { 14 | opacity: 0; 15 | transition: opacity 0.3s ease-in; 16 | } 17 | } 18 | 19 | div.transition-group { 20 | height: 100%; 21 | width: 100%; 22 | overflow: hidden; 23 | } 24 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/style.less: -------------------------------------------------------------------------------- 1 | .field { 2 | display: flex; 3 | flex-direction: column; 4 | cursor: pointer; 5 | } 6 | 7 | .pass .label { 8 | color: var(--pass-label-color); 9 | text-transform: uppercase; 10 | font-size: 7pt; 11 | cursor: pointer; 12 | font-weight: 400; 13 | } 14 | 15 | .pass .value { 16 | color: var(--pass-foreground-color); 17 | font-size: 8pt; 18 | cursor: pointer; 19 | } 20 | -------------------------------------------------------------------------------- /src/Pass/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PassMixedProps } from ".."; 2 | 3 | export { default as BoardingPass } from "./BoardingPass"; 4 | export { default as Coupon } from "./Coupon"; 5 | export { default as EventTicket } from "./EventTicket"; 6 | export { default as Generic } from "./Generic"; 7 | export { default as StoreCard } from "./StoreCard"; 8 | 9 | export type LayoutSignature = React.FunctionComponent; 10 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../App/common.less"; 2 | 3 | #fields-properties-edit-page { 4 | .scrollableWithoutScrollbars(y); 5 | 6 | & > .field-preview { 7 | border-bottom: 1px solid #2f2f2f; 8 | margin-bottom: 20px; 9 | padding: 0px 10px 20px 10px; 10 | background-color: rgba(28, 28, 28, 0.96); 11 | position: sticky; 12 | top: 56px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/FieldRow/style.less: -------------------------------------------------------------------------------- 1 | .fields-row { 2 | display: flex; 3 | flex-direction: row; 4 | width: 100%; 5 | flex-wrap: wrap; 6 | overflow: hidden; 7 | margin-top: 10px; 8 | justify-content: space-between; 9 | min-height: 20px; 10 | max-height: 25px; /* Needed to allow flex-wrap and overflow hide overflowing elements */ 11 | 12 | & .empty-field { 13 | border-radius: 2px; 14 | height: 22px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CollectionActivationMiddleware } from "./CollectionActivationMiddleware"; 2 | export { default as CollectionEditUrlMiddleware } from "./CollectionEditUrlMiddleware"; 3 | export { default as PurgeMiddleware } from "./PurgeMiddleware"; 4 | export { default as CreationMiddleware } from "./CreationMiddleware"; 5 | export { default as LocalForageSaveMiddleware } from "./LocalForageSaveMiddleware"; 6 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldOptionsBar/style.less: -------------------------------------------------------------------------------- 1 | .field-options-bar { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 5px; 5 | margin-top: 20px; 6 | 7 | & > * { 8 | display: flex; 9 | align-items: center; 10 | } 11 | 12 | & svg { 13 | width: 25px; 14 | cursor: pointer; 15 | 16 | &.danger { 17 | fill: #ff6363; 18 | width: 22px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Configurator/LanguageSelectionButton/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | interface Props { 5 | label: string; 6 | onClick(): void; 7 | } 8 | 9 | export default function LanguageSelectionButton(props: Props) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/TextField/style.less: -------------------------------------------------------------------------------- 1 | .text-field { 2 | color: var(--pass-foreground-color); 3 | height: 100%; 4 | display: flex; 5 | align-items: center; 6 | border: 1px dashed var(--pass-foreground-color); // development only or advanced mode 7 | flex-grow: 1; 8 | min-width: 0; // for ellipsis truncating 9 | cursor: pointer; 10 | 11 | & span { 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | overflow: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/style.less: -------------------------------------------------------------------------------- 1 | @even-elements-color: #272727; 2 | 3 | .field-edit-item { 4 | display: flex; 5 | flex-direction: column; 6 | position: relative; 7 | min-height: 170px; 8 | padding: 5px; 9 | /* To allow scrolling */ 10 | flex-shrink: 0; 11 | 12 | &:last-child { 13 | border-bottom: 1px solid #333; 14 | } 15 | 16 | &:nth-child(2n) { 17 | background-color: @even-elements-color; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/navigation.utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { PassMixedProps } from "@pkvd/pass"; 2 | 3 | export { default as navigable } from "./navigable.hoc"; 4 | export { default as usePageRelation } from "./usePageRelation.hook"; 5 | export { default as usePagesAmount } from "./usePagesAmount.hook"; 6 | 7 | export type { NavigableProps } from "./navigable.hoc"; 8 | export interface PageProps { 9 | name: string | keyof PassMixedProps; 10 | onBack: Function; 11 | } 12 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldOptionsBar/FieldOrderHandler/style.less: -------------------------------------------------------------------------------- 1 | .field-order-handler { 2 | & svg { 3 | width: 15px; 4 | fill: #e6e6e6; 5 | margin: 0 10px; 6 | cursor: pointer; 7 | 8 | &.disabled { 9 | pointer-events: none; 10 | fill: #333333; 11 | } 12 | 13 | &:nth-child(1) { 14 | transform: rotate(-90deg); 15 | } 16 | 17 | &:nth-child(2) { 18 | transform: rotate(90deg); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/BackFields/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import FieldsRow from "../FieldRow"; 4 | import { PassField } from "../../../constants"; 5 | 6 | interface Props { 7 | data: PassField[]; 8 | } 9 | 10 | export default function Backfields(props: Props) { 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerPlaceholder/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | export default function FieldsDrawerPlaceholder() { 5 | return ( 6 |
7 | 8 | 9 | ¯\_(ツ)_/¯ 10 | 11 | 12 |

There are no fields here yet.

13 |

What about starting adding some?

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/ColorPanel/style.less: -------------------------------------------------------------------------------- 1 | .panel.color { 2 | // Overrides for react-color styles 3 | .color-selector { 4 | background-color: #1c1c1c !important; 5 | 6 | & > div { 7 | & > span ~ div { 8 | background-color: #282828 !important; 9 | } 10 | 11 | & > div ~ div > input { 12 | background-color: #333; 13 | box-shadow: #3c3c3c 0px 0px 0px 1px inset !important; 14 | color: #e6e6e6 !important; 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Pass/InteractionContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { onRegister } from "./layouts/sections/useRegistrations"; 3 | 4 | /** 5 | * InteractionContext is a way to directly pass the registration 6 | * method through all the components in Pass. 7 | * 8 | * It is used to have a function, provided by the Configurator, 9 | * that will register all the components that will use the hook 10 | * `useRegistrations`. 11 | */ 12 | 13 | export default React.createContext(undefined); 14 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | /** The keys are the same of WalletPassFormat */ 2 | export enum PassKind { 3 | BOARDING_PASS = "boardingPass", 4 | STORE = "storeCard", 5 | COUPON = "coupon", 6 | GENERIC = "generic", 7 | EVENT = "eventTicket" 8 | } 9 | 10 | export enum FieldKind { 11 | TEXT = "text", 12 | IMAGE = "image", 13 | COLOR = "color", 14 | FIELDS = "fields", 15 | SWITCH = "switch", 16 | JSON = "json" 17 | } 18 | 19 | export type StylingProps = Pick, "className" | "style"> 20 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/ImagePanel/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // Arrow by Kirsh from the Noun Project (edited) 4 | // https://thenounproject.com/term/arrow/1256496 5 | 6 | export function ArrowIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // Arrow by Kirsh from the Noun Project (edited) 4 | // https://thenounproject.com/term/arrow/1256496 5 | 6 | export default function FieldsArrowIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/FieldsPanel/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // Arrow by Kirsh from the Noun Project (edited) 4 | // https://thenounproject.com/term/arrow/1256496 5 | 6 | export function FieldsArrowIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/ImagePanel/style.less: -------------------------------------------------------------------------------- 1 | .panel.image { 2 | & > div { 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | color: #e6e6e6; 7 | border: 5px solid #333; 8 | border-radius: 5px; 9 | box-sizing: border-box; 10 | width: 100%; 11 | height: 100px; 12 | padding-left: 20px; 13 | cursor: pointer; 14 | 15 | & > .go-editor-arrow { 16 | width: 32px; 17 | height: 32px; 18 | fill: #e6e6e6; 19 | padding-right: 20px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/useFallback.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import EmptyField from "./EmptyField"; 3 | 4 | /** 5 | * Hook that uses the same form of useMemo 6 | * to replace the element with a fallback one 7 | * if all dependencies are not "truthy". 8 | * 9 | * @param create 10 | * @param deps 11 | * @returns 12 | */ 13 | 14 | export default function useFallback(create: () => T, deps: any[]) { 15 | if (deps.every((dep) => !Boolean(dep))) { 16 | return ; 17 | } 18 | 19 | return create(); 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "sourceMap": true, 5 | "module": "ES6", 6 | "esModuleInterop": true, 7 | "target": "ES2016", 8 | "newLine": "LF", 9 | "lib": [ 10 | "dom", 11 | "es2015" 12 | ], 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "importHelpers": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@pkvd/pass": [ 19 | "src/Pass" 20 | ], 21 | "@pkvd/store": [ 22 | "src/store" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/FieldsPanel/style.less: -------------------------------------------------------------------------------- 1 | .panel.fields { 2 | color: #fff; 3 | display: flex; 4 | justify-content: space-between; 5 | height: 55px; 6 | 7 | div.cta-edit { 8 | flex-grow: 1; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | cursor: pointer; 13 | 14 | .col-left { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | span { 20 | font-size: 15px; 21 | } 22 | 23 | & svg { 24 | width: 22px; 25 | margin-left: 5px; 26 | fill: #e6e6e6; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/navigation.utils/usePagesAmount.hook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { registerListener, reset } from "./navigation.memory"; 3 | 4 | export default function usePagesAmount() { 5 | const [amount, updateAmount] = React.useState(0); 6 | 7 | React.useEffect(() => { 8 | /** Reset pages on unmound*/ 9 | return reset; 10 | }, []); 11 | 12 | const onUpdate = React.useCallback((amount: number) => { 13 | updateAmount(amount); 14 | }, []); 15 | 16 | React.useEffect(() => { 17 | registerListener(onUpdate); 18 | }, []); 19 | 20 | return amount; 21 | } 22 | -------------------------------------------------------------------------------- /src/Configurator/ModalBase/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default function ModalCloseIcon(props: React.SVGProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/Coupon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Layouts, PassMixedProps } from "@pkvd/pass"; 3 | import useAlternativesRegistration from "../useAlternativesRegistration"; 4 | import { PassKind } from "../../../model"; 5 | 6 | /** 7 | * Layout proxy with alternatives registration capability. 8 | * 9 | * @param props 10 | * @returns 11 | */ 12 | 13 | export default function Coupon(props: PassMixedProps) { 14 | useAlternativesRegistration(PassKind.COUPON, { 15 | name: "Coupon Pass", 16 | specificProps: {}, 17 | }); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/StoreCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Layouts, PassMixedProps } from "@pkvd/pass"; 3 | import useAlternativesRegistration from "../useAlternativesRegistration"; 4 | import { PassKind } from "../../../model"; 5 | 6 | /** 7 | * Layout proxy with alternatives registration capability. 8 | * 9 | * @param props 10 | * @returns 11 | */ 12 | 13 | export default function StoreCard(props: PassMixedProps) { 14 | useAlternativesRegistration(PassKind.STORE, { 15 | name: "StoreCard", 16 | specificProps: {}, 17 | }); 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/Pass/useCSSCustomProperty.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default function useCSSCustomProperty( 4 | ref: React.RefObject, 5 | name: string, 6 | value: string 7 | ) { 8 | React.useLayoutEffect(() => { 9 | const { current } = ref; 10 | 11 | if (!current) { 12 | return; 13 | } 14 | 15 | if (value.includes("://")) { 16 | value = `url(${value})`; 17 | } 18 | 19 | const prefixedName = `--pass-${name}`; 20 | 21 | if (current.style.getPropertyValue(prefixedName) === value) { 22 | return; 23 | } 24 | 25 | current.style.setProperty(prefixedName, value); 26 | }, [value]); 27 | } 28 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/useClickEvent.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FieldSelectHandler } from "../sections/useRegistrations"; 3 | 4 | export default function useClickEvent( 5 | onClick: FieldSelectHandler, 6 | element: React.ReactElement 7 | ) { 8 | if (!onClick) { 9 | return element; 10 | } 11 | 12 | if (element.type === React.Fragment) { 13 | // Mapping the fragment props on children 14 | const children = React.Children.map(element.props.children, (node) => 15 | React.cloneElement(node, { onClick }) 16 | ); 17 | 18 | return React.cloneElement(element, {}, children); 19 | } 20 | 21 | return React.cloneElement(element, { onClick }); 22 | } 23 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/FieldLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { getCSSFromFieldProps, FieldProperties, FieldTypes } from "./fieldCommons"; 3 | import { SelectableComponent } from "../../sections/useRegistrations"; 4 | 5 | type LabelProps = Partial> & { 6 | fieldData: Partial>; 7 | }; 8 | 9 | export default function FieldLabel(props: LabelProps) { 10 | const { fieldData, onClick } = props; 11 | const style = getCSSFromFieldProps(fieldData, "label"); 12 | 13 | return ( 14 | 15 | {fieldData.label || ""} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/FieldPropertiesEditList/FieldPropertyPanels/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | name: string; 5 | value?: boolean; 6 | onValueChange(prop: string, value: T): void; 7 | } 8 | 9 | export default function FieldCheckboxPropertyPanel(props: Props) { 10 | return ( 11 |
12 | 13 | props.onValueChange(props.name, ev.currentTarget.checked)} 18 | /> 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import FieldsArrowIcon from "../../icons"; 4 | import CapitalHeaderTitle from "../CapitalHeaderTitle"; 5 | import { PageProps } from "../../../navigation.utils"; 6 | 7 | interface Props extends Partial> { 8 | name?: string; 9 | } 10 | 11 | export default function PageHeader(props: React.PropsWithChildren) { 12 | return ( 13 |
14 |
props.onBack()}> 15 | 16 | Back 17 |
18 | {props.name && } 19 | {props.children} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/Configurator/LanguageSelectionButton/style.less: -------------------------------------------------------------------------------- 1 | & .language-select-btn { 2 | cursor: pointer; 3 | 4 | background: transparent; 5 | border: 0; 6 | color: #e6e6e6; 7 | outline: none; 8 | 9 | padding: 5px 15px; 10 | border: 0.5px solid #848484; 11 | border-radius: 23px; /** Same border radius as switcher **/ 12 | transition: all 0.5s ease-in-out; 13 | 14 | &:hover { 15 | background-color: #313131; 16 | } 17 | 18 | &:active { 19 | border-color: #e6e6e6; 20 | } 21 | 22 | &:after { 23 | content: ""; 24 | display: inline-block; 25 | width: 0; 26 | height: 0; 27 | border-style: solid; 28 | border-width: 5px 5px 0 5px; 29 | border-color: #e6e6e6 transparent transparent transparent; 30 | vertical-align: middle; 31 | margin-left: 5px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/useContentSavingHandler.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | 3 | export type ContentSavingHandler = [content: T, handler: (content?: T) => void]; 4 | 5 | export default function useContentSavingHandler( 6 | onValueChange: (name: string, content: T) => void, 7 | panelName: string, 8 | initialContent?: T 9 | ): ContentSavingHandler { 10 | const [content, setContent] = useState(initialContent || null); 11 | 12 | const onContentChangedHandlerRef = useCallback( 13 | (newContent?: T) => { 14 | if (!newContent && !content) { 15 | return; 16 | } 17 | 18 | setContent(newContent); 19 | onValueChange(panelName, newContent); 20 | }, 21 | [content] 22 | ); 23 | 24 | return [content, onContentChangedHandlerRef]; 25 | } 26 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../App/common.less"; 2 | 3 | .list-element { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | border-bottom: 1px solid #333; 8 | overflow: hidden; 9 | flex-grow: 1; 10 | 11 | & > .panels-list { 12 | padding-top: 10px; 13 | background-color: #282828; 14 | .scrollableWithoutScrollbars(y); 15 | } 16 | 17 | &::after { 18 | height: 60px; 19 | bottom: 56px; 20 | content: ""; 21 | position: absolute; 22 | right: 0; 23 | left: 0; 24 | opacity: 0; 25 | background: linear-gradient(to top, rgb(28 28 28) 0%, transparent 40%); 26 | transition: opacity 0.5s ease-in-out; 27 | pointer-events: none; 28 | } 29 | 30 | &.not-enough::after { 31 | opacity: 1; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/EventTicket.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Layouts, PassMixedProps } from "@pkvd/pass"; 3 | import useAlternativesRegistration from "../useAlternativesRegistration"; 4 | import { PassKind } from "../../../model"; 5 | 6 | /** 7 | * Layout proxy with alternatives registration capability. 8 | * 9 | * @param props 10 | * @returns 11 | */ 12 | 13 | export default function EventTicket(props: PassMixedProps) { 14 | useAlternativesRegistration( 15 | PassKind.EVENT, 16 | { 17 | name: "With background image", 18 | specificProps: { 19 | backgroundImage: null, 20 | }, 21 | }, 22 | { 23 | name: "With strip image", 24 | specificProps: { 25 | stripImage: null, 26 | }, 27 | } 28 | ); 29 | 30 | return ; 31 | } 32 | -------------------------------------------------------------------------------- /partners-templates/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is literally the index of templates available in the folder. 3 | * It is used as brigde to pick them all up and import them in the code. 4 | * 5 | * Partners Templates are code templates written in handlebars. They get 6 | * precompiled through handlebars-loader as functions, so we can generate 7 | * partners code when user is going to export the pass. 8 | * 9 | * Every exported name **must** match the name saved in `index.json`. 10 | * 11 | * `index.json` acts as a supplementary index to contain configuration for 12 | * the code to show the tabs and the processed template... but json is not 13 | * allowed to have comments. 14 | */ 15 | 16 | // @ts-ignore - handled by webpack 17 | import passkitGenerator from "./passkit-generator.hbs"; 18 | 19 | export { 20 | passkitGenerator 21 | }; 22 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Strip/style.less: -------------------------------------------------------------------------------- 1 | .strip-primaryFields { 2 | position: relative; 3 | /** Primary Fields overflow the padding */ 4 | width: calc(100% + 20px); 5 | height: 90px; 6 | 7 | & > .row { 8 | position: absolute; 9 | display: flex; 10 | flex-direction: column; 11 | height: 100%; 12 | justify-content: flex-start; 13 | box-sizing: border-box; 14 | padding: 15px 10px; 15 | width: 100%; 16 | 17 | & > .value { 18 | text-transform: none; 19 | font-size: 29pt; 20 | line-height: 29pt; 21 | white-space: nowrap; 22 | } 23 | 24 | & > .label { 25 | text-transform: none; 26 | font-size: 9pt; 27 | } 28 | } 29 | 30 | & > .image-field { 31 | width: 100%; 32 | height: 100%; 33 | 34 | & > img { 35 | object-fit: fill; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/components/Header/style.less: -------------------------------------------------------------------------------- 1 | #pages-navigator > .page { 2 | & header { 3 | display: flex; 4 | justify-content: space-between; 5 | flex-shrink: 0; 6 | flex-grow: 0; 7 | height: 50px; 8 | align-items: center; 9 | border-bottom: 1px solid rgb(47, 47, 47); 10 | padding: 5px 5px 0 5px; 11 | color: #e6e6e6; 12 | background-color: #1c1c1c; 13 | position: sticky; 14 | top: 0; 15 | 16 | & .back { 17 | display: flex; 18 | align-items: center; 19 | cursor: pointer; 20 | 21 | & svg { 22 | &#back { 23 | width: 22px; 24 | transform: rotate(180deg); 25 | fill: #e6e6e6; 26 | margin-right: 5px; 27 | } 28 | } 29 | } 30 | 31 | & h4 { 32 | font-weight: inherit; 33 | margin: 0; 34 | // "Ignoring" the "back" text and centering a little bit more 35 | margin-right: 25px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/FieldPropertiesEditList/FieldPropertyPanels/String.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import CommittableTextInput from "../../../../../CommittableTextInput"; 3 | 4 | interface Props { 5 | name: string; 6 | value?: string | number; 7 | placeholder?: string; 8 | onValueChange(prop: string, value: T): void; 9 | } 10 | 11 | export default function FieldStringPropertyPanel(props: Props) { 12 | const onCommit = React.useCallback((value: string) => { 13 | props.onValueChange(props.name, value); 14 | }, []); 15 | 16 | return ( 17 |
18 | 19 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/TextField/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { SelectableComponent } from "../../sections/useRegistrations"; 4 | import { createClassName } from "../../../../utils"; 5 | import useFallback from "../useFallback"; 6 | import useClickEvent from "../useClickEvent"; 7 | 8 | export interface TextFieldProps extends Partial { 9 | content?: string; 10 | className?: string; 11 | } 12 | 13 | export default function TextField(props: TextFieldProps) { 14 | const { content, className: sourceClassName, onClick } = props; 15 | 16 | return useClickEvent( 17 | onClick, 18 | useFallback(() => { 19 | const className = createClassName(["text-field", sourceClassName]); 20 | 21 | return ( 22 |
23 | {content} 24 |
25 | ); 26 | }, [content]) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/index.tsx: -------------------------------------------------------------------------------- 1 | import "./style.less"; 2 | import { PassMixedProps } from "@pkvd/pass"; 3 | import { FieldKind } from "../../../../../model"; 4 | import { DataGroup } from ".."; 5 | 6 | export { default as TextPanel } from "./TextPanel"; 7 | export { default as ColorPanel } from "./ColorPanel"; 8 | export { default as FieldsPanel } from "./FieldsPanel"; 9 | export { default as ImagePanel } from "./ImagePanel"; 10 | 11 | export interface SharedPanelProps { 12 | name: string; 13 | data: Omit; 14 | isSelected?: boolean; 15 | onValueChange?(name: string, data: T): void; 16 | } 17 | 18 | export interface FieldDetails { 19 | name: string | keyof PassMixedProps; 20 | kind: FieldKind; 21 | group: DataGroup; 22 | mockable?: boolean; 23 | tooltipText?: string; 24 | disabled?: boolean; 25 | required?: boolean; 26 | jsonKeys?: string[]; 27 | } 28 | -------------------------------------------------------------------------------- /src/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, CombinedState, combineReducers } from "redux"; 2 | import { State } from "."; 3 | import pass from "./pass"; 4 | import media from "./media"; 5 | import projectOptions from "./projectOptions"; 6 | import translations from "./translations"; 7 | import * as forage from "./forage"; 8 | 9 | const applicationReducers = combineReducers({ 10 | pass, 11 | media, 12 | projectOptions, 13 | translations, 14 | }); 15 | 16 | export default function (state: CombinedState, action: AnyAction) { 17 | if (action.type === forage.RESET) { 18 | // Making reducers to fallback to their initial state 19 | return applicationReducers(undefined, action); 20 | } 21 | 22 | if (action.type === forage.INIT) { 23 | const { snapshot } = action as forage.Actions.Init; 24 | return applicationReducers(snapshot, action); 25 | } 26 | 27 | return applicationReducers(state, action); 28 | } 29 | -------------------------------------------------------------------------------- /src/Configurator/OptionsBar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { ShowMoreIcon, EyeVisibleIcon, EyeInvisibleIcon, TranslationsIcon } from "./icons"; 4 | 5 | interface Props { 6 | isEmptyVisible: boolean; 7 | rotatePass(): void; 8 | toggleEmptyVisibility(): void; 9 | toggleTranslationsModal(): void; 10 | } 11 | 12 | export default function OptionsBar(props: Props) { 13 | const EyeIcon = props.isEmptyVisible ? EyeVisibleIcon : EyeInvisibleIcon; 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | props.toggleEmptyVisibility()} /> 22 |
23 |
24 | props.rotatePass()} /> 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Barcodes/style.less: -------------------------------------------------------------------------------- 1 | .barcode { 2 | display: flex; 3 | box-sizing: border-box; 4 | justify-content: center; 5 | width: 100%; 6 | 7 | grid-column: ~"2 / 3"; 8 | grid-row: ~"2 / 3"; 9 | 10 | /* Using another div to contain fallback barcodes */ 11 | &.PKBarcodeFormatNone, 12 | &.PKBarcodeFormatRectangle { 13 | &.rect > div { 14 | width: 100%; 15 | } 16 | } 17 | 18 | &.content > div { 19 | background-color: #fff; 20 | padding: 7px; 21 | font-size: 12px; 22 | text-align: center; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | padding: 4px 8px; 27 | } 28 | 29 | & > div { 30 | border-radius: 3px; 31 | 32 | & > .fallback { 33 | background-color: #cecece; 34 | border-radius: 2px; 35 | 36 | &.square { 37 | width: 100px; 38 | height: 100px; 39 | } 40 | 41 | &.bar { 42 | height: 35px; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/ImageField/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SelectableComponent } from "../../sections/useRegistrations"; 3 | import { createClassName } from "../../../../utils"; 4 | import useFallback from "../useFallback"; 5 | import useClickEvent from "../useClickEvent"; 6 | 7 | export interface ImageFieldProps extends Partial { 8 | className?: string; 9 | width?: string; 10 | height?: string; 11 | src?: string; 12 | } 13 | 14 | export default function ImageField(props: ImageFieldProps) { 15 | const { src, width, height, className: sourceClassName, onClick } = props; 16 | 17 | return useClickEvent( 18 | onClick, 19 | useFallback(() => { 20 | const className = createClassName(["image-field", sourceClassName]); 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }, [src]) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/ImagePanel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { PassMediaProps } from "@pkvd/pass"; 4 | import { SharedPanelProps } from ".."; 5 | import CapitalHeaderTitle from "../../../components/CapitalHeaderTitle"; 6 | import { ArrowIcon } from "./icons"; 7 | import { FieldKind } from "../../../../../../model"; 8 | 9 | interface ImagePanelProps extends SharedPanelProps { 10 | name: T; 11 | onSelect(mediaName: T): void; 12 | } 13 | 14 | export default function ImagePanel(props: ImagePanelProps) { 15 | return ( 16 |
17 | 18 |
props.onSelect(props.name)}> 19 | Open media editor 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/navigation.utils/usePageRelation.hook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { v1 as uuid } from "uuid"; 3 | import { addPage, removePageChain, sendUpdates } from "./navigation.memory"; 4 | 5 | export default function usePageRelation(): [boolean, Function, Function, T] { 6 | const [isOpen, setPageOpenness] = React.useState(false); 7 | const pageID = React.useRef(uuid()); 8 | const contextualProps = React.useRef(); 9 | 10 | const openPage = React.useCallback((pageProps: T) => { 11 | contextualProps.current = pageProps; 12 | setPageOpenness(true); 13 | addPage(pageID.current); 14 | sendUpdates(); 15 | }, []); 16 | 17 | const closePage = React.useCallback(() => { 18 | contextualProps.current = undefined; 19 | setPageOpenness(false); 20 | removePageChain(pageID.current); 21 | sendUpdates(); 22 | }, []); 23 | 24 | return [isOpen, openPage, closePage, contextualProps.current]; 25 | } 26 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/Generic.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Layouts, PassMixedProps, Constants } from "@pkvd/pass"; 3 | import useAlternativesRegistration from "../useAlternativesRegistration"; 4 | import { PassKind } from "../../../model"; 5 | 6 | const { PKBarcodeFormat } = Constants; 7 | 8 | /** 9 | * Layout proxy with alternatives registration capability. 10 | * 11 | * @param props 12 | * @returns 13 | */ 14 | 15 | export default function Generic(props: PassMixedProps) { 16 | useAlternativesRegistration( 17 | PassKind.GENERIC, 18 | { 19 | name: "With rectangular barcode", 20 | specificProps: { 21 | barcode: { 22 | format: PKBarcodeFormat.Rectangle, 23 | }, 24 | }, 25 | }, 26 | { 27 | name: "With square barcode", 28 | specificProps: { 29 | barcode: { 30 | format: PKBarcodeFormat.Square, 31 | }, 32 | }, 33 | } 34 | ); 35 | 36 | return ; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a list of classNames only if they are 3 | * truthy if converted to boolean, along with some 4 | * default ones. 5 | * 6 | * @param staticClassNames 7 | * @param candidates 8 | */ 9 | 10 | type CandidateClassNameList = { 11 | [key: string]: any; 12 | } 13 | 14 | export function createClassName(staticClassNames: string[], candidates: CandidateClassNameList = {}) { 15 | const keys = Object.keys(candidates); 16 | return [ 17 | ...staticClassNames, 18 | ...keys.filter(key => Boolean(candidates[key])) 19 | ].join(" ").trim(); 20 | } 21 | 22 | /** 23 | * Converts a blob object to arrayBuffer. 24 | * Includes support to a workaround for Safari and all the 25 | * other browsers that do not support Blob.prototype.arrayBuffer 26 | */ 27 | 28 | export async function getArrayBuffer(blob: Blob) { 29 | if (Blob.prototype.arrayBuffer instanceof Function) { 30 | return await blob.arrayBuffer(); 31 | } else { 32 | return await (new Response(blob)).arrayBuffer(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Configurator/ModalBase/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ModalCloseIcon from "./icons"; 3 | import "./style.less"; 4 | 5 | /** 6 | * Generic component to create a modal window 7 | */ 8 | 9 | export interface ModalProps { 10 | closeModal(): void; 11 | contentUniqueID: string; 12 | } 13 | 14 | export default function Modal({ 15 | children, 16 | closeModal, 17 | contentUniqueID, 18 | }: React.PropsWithChildren) { 19 | const onKeyDownEventRef = React.useRef( 20 | ({ key }: KeyboardEvent) => key === "Escape" && closeModal() 21 | ); 22 | 23 | React.useEffect(() => { 24 | document.body.addEventListener("keydown", onKeyDownEventRef.current); 25 | return () => document.body.removeEventListener("keydown", onKeyDownEventRef.current); 26 | }); 27 | 28 | return ( 29 |
30 | 31 |
32 | {children} 33 |
34 |
closeModal?.()} /> 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // add by Harper from the Noun Project 4 | // https://thenounproject.com/term/add/1623623 5 | 6 | export function FieldsAddIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | // dots by Andrejs Kirma from the Noun Project (edited) 16 | // https://thenounproject.com/term/dots/2915594 17 | 18 | export function MoreFieldsBelowIcon(props: React.SVGProps) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/TextPanel/style.less: -------------------------------------------------------------------------------- 1 | .panel.text { 2 | & > label { 3 | cursor: pointer; 4 | display: flex; 5 | justify-content: space-between; 6 | 7 | & .required::after { 8 | content: "required"; 9 | color: #fff; 10 | font-size: 10px; 11 | background-color: #444; 12 | border: 0.5px solid #696969b5; 13 | padding: 5px 7px; 14 | border-radius: 2px; 15 | position: relative; 16 | bottom: 2px; 17 | } 18 | } 19 | 20 | & > input { 21 | background-color: transparent; 22 | width: 100%; 23 | outline: none; 24 | font-size: 14px; 25 | padding: 7px; 26 | box-sizing: border-box; 27 | border: none; 28 | border-bottom: 1px solid rgb(66, 66, 66); 29 | transition: border-bottom 0.5s ease-in-out; 30 | color: #cacaca; 31 | 32 | &::placeholder { 33 | font-style: italic; 34 | font-size: 14px; 35 | color: #989898; 36 | } 37 | 38 | &::selection { 39 | background-color: #9a9a9a; 40 | color: #fff; 41 | } 42 | 43 | &:focus { 44 | border-bottom: 1px solid #cacaca; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/FieldsPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { SharedPanelProps } from ".."; 5 | import { FieldsArrowIcon } from "./icons"; 6 | import CapitalHeaderTitle from "../../../components/CapitalHeaderTitle"; 7 | import useContentSavingHandler from "../useContentSavingHandler"; 8 | import { FieldKind } from "../../../../../../model"; 9 | 10 | type PassField = Constants.PassField; 11 | 12 | interface Props extends SharedPanelProps { 13 | onSelect(name: string): void; 14 | value?: PassField[]; 15 | } 16 | 17 | export default function FieldPanel(props: Props) { 18 | return ( 19 |
20 |
props.onSelect(props.name)}> 21 |
22 | 23 | {`${props.value?.length ?? 0} fields for this area`} 24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/public/styles.less: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(51, 51, 51); 3 | margin: 0; 4 | padding: 0; 5 | font-family: "SF Display", sans-serif; 6 | font-weight: 200; 7 | 8 | /* Preventing inertia scrolling*/ 9 | overflow: hidden; 10 | } 11 | 12 | img { 13 | max-width: 100%; 14 | } 15 | 16 | #app { 17 | display: flex; 18 | width: 100%; 19 | } 20 | 21 | #root { 22 | display: flex; 23 | height: 100vh; 24 | align-items: center; 25 | } 26 | 27 | @font-face { 28 | font-family: "SF Display"; 29 | font-weight: 100; 30 | src: url(../../assets/SF-Pro-Display-Thin.otf); 31 | font-display: fallback; 32 | } 33 | 34 | @font-face { 35 | font-family: "SF Display"; 36 | font-weight: 200; 37 | src: url(../../assets/SF-Pro-Display-Light.otf); 38 | font-display: fallback; 39 | } 40 | 41 | @font-face { 42 | font-family: "SF Display"; 43 | font-weight: 300; 44 | src: url(../../assets/SF-Pro-Display-Regular.otf); 45 | font-display: fallback; 46 | } 47 | 48 | @font-face { 49 | font-family: "SF Display"; 50 | font-weight: 400; 51 | src: url(../../assets/SF-Pro-Display-Medium.otf); 52 | font-display: fallback; 53 | } 54 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/FieldPropertiesEditList/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../../App/common.less"; 2 | 3 | .field-properties-edit-list { 4 | display: flex; 5 | flex-direction: column; 6 | flex-grow: 1; 7 | width: 100%; 8 | margin-bottom: 30%; 9 | .scrollableWithoutScrollbars(y); 10 | 11 | & > * { 12 | /** 13 | * Here we are using flex with wrap to keep inputs on new line 14 | * but also keep checkboxes on the same line 15 | */ 16 | display: flex; 17 | justify-content: space-between; 18 | flex-wrap: wrap; 19 | margin: 5px; 20 | padding: 5px; 21 | 22 | & label { 23 | color: #e6e6e6; 24 | margin-right: 25px; 25 | cursor: pointer; 26 | } 27 | 28 | & input:not([type="checkbox"]), 29 | & select { 30 | outline: none; 31 | border: none; 32 | background: transparent; 33 | border-radius: 0; 34 | border-bottom: 1px solid #e6e6e6; 35 | color: #e6e6e6; 36 | width: 100%; 37 | padding-bottom: 8px; 38 | margin-top: 12px; 39 | font-size: 14px; 40 | } 41 | 42 | & select option { 43 | background-color: #333; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexander Cerutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/useAlternativesRegistration.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from "@pkvd/pass"; 3 | import { PassKind } from "../../model"; 4 | 5 | interface PassAlternative { 6 | name: string; 7 | specificProps: Partial; 8 | } 9 | 10 | type PassKindsAlternatives = { 11 | [key in PassKind]?: PassAlternative[]; 12 | }; 13 | 14 | const alternativesList: PassKindsAlternatives = {}; 15 | 16 | /** 17 | * Hook to save a list of possible alternatives of a 18 | * pass kind, only on the first rendering 19 | * 20 | * @param kind 21 | * @param alternatives 22 | */ 23 | 24 | export default function useAlternativesRegistration( 25 | kind: PassKind, 26 | ...alternatives: PassAlternative[] 27 | ) { 28 | React.useEffect(() => { 29 | alternativesList[kind] = [ ...alternatives ]; 30 | }, []); 31 | } 32 | 33 | /** 34 | * Retrieves the alternatives list for a specified kind 35 | * 36 | * @param kind 37 | * @returns 38 | */ 39 | 40 | export function getAlternativesByKind(kind: PassKind) { 41 | if (!kind) { 42 | return undefined; 43 | } 44 | 45 | return alternativesList[kind]; 46 | } 47 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerJSONEditor/style.less: -------------------------------------------------------------------------------- 1 | #pages-navigator > .page > .fields-preview-page > .json-editor { 2 | display: flex; 3 | flex-grow: 1; 4 | flex-direction: column; 5 | 6 | & pre { 7 | margin: 0; 8 | padding-left: 4px; 9 | 10 | & code { 11 | text-shadow: none; 12 | } 13 | } 14 | 15 | & textarea { 16 | background-color: #272727; 17 | border: none; 18 | resize: none; 19 | flex-grow: 1; 20 | color: #e6e6e6; 21 | padding: 0 0 0 40px; 22 | tab-size: 4; 23 | 24 | &:focus { 25 | outline: 1px solid #333333; 26 | } 27 | } 28 | 29 | & .json-validity-alert { 30 | min-height: 40px; 31 | position: relative; 32 | display: flex; 33 | align-items: center; 34 | padding: 10px; 35 | 36 | &.valid { 37 | background-color: rgb(16, 70, 16); 38 | 39 | &::before { 40 | content: "Valid JSON! Safe to go"; 41 | } 42 | } 43 | 44 | &.invalid { 45 | background-color: #a00000; 46 | 47 | &::before { 48 | content: "Invalid JSON. Going back now will result in your latest edits to get discarded. Last successful check changes will be used instead."; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/getFilteredFieldData.ts: -------------------------------------------------------------------------------- 1 | import { PassField } from "../../../constants"; 2 | 3 | /** 4 | * Tries to get data from the specified source 5 | * if it is available and the array has elements 6 | * in it. Otherwise creates an array of empty 7 | * objects to fallback. 8 | * 9 | * @param data 10 | * @param fallbackAmount 11 | */ 12 | 13 | export function getFilteredFieldData( 14 | data: PassField[] = [], 15 | minAmount: number = 0, 16 | maxAmount: number = 0 17 | ) { 18 | if (!data.length) { 19 | return createFilledPassFieldArray(minAmount); 20 | } 21 | 22 | /** 23 | * Filtering data that really have something to show 24 | * and, of those, the ones that can be really showed 25 | * on the pass 26 | */ 27 | 28 | const showableFields = data 29 | .filter(({ value, label }) => value || label) 30 | .slice(0, (maxAmount > 0 && maxAmount) || undefined); 31 | 32 | if (!showableFields.length) { 33 | return createFilledPassFieldArray(minAmount); 34 | } 35 | 36 | return showableFields; 37 | } 38 | 39 | function createFilledPassFieldArray(slots: number) { 40 | return new Array(slots).fill({} as PassField); 41 | } 42 | -------------------------------------------------------------------------------- /src/store/middlewares/CreationMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareAPI, AnyAction, Dispatch } from "redux"; 2 | import { State } from ".."; 3 | import * as Store from ".."; 4 | 5 | type AllowedActions = Store.Media.Actions.EditCollection | Store.Translations.Actions.Add; 6 | 7 | export default function CreationMiddleware(store: MiddlewareAPI) { 8 | return (next: Dispatch) => (action: AllowedActions) => { 9 | let { media, translations } = store.getState(); 10 | 11 | if (action.type === Store.Media.EDIT_COLLECTION) { 12 | if (!(action.mediaLanguage in media)) { 13 | store.dispatch(Store.Media.Create(action.mediaLanguage)); 14 | media = store.getState().media; 15 | } 16 | 17 | if (!(action.mediaName in media[action.mediaLanguage])) { 18 | store.dispatch(Store.Media.Init(action.mediaName, action.mediaLanguage)); 19 | } 20 | } else if (action.type === Store.Translations.ADD) { 21 | if (!(action.translationLanguage in translations)) { 22 | store.dispatch(Store.Translations.Init(action.translationLanguage)); 23 | translations = store.getState().translations; 24 | } 25 | } 26 | 27 | return next(action); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/Configurator/Switcher/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | interface Props { 5 | labelPosition: "before" | "after"; 6 | checked?: boolean; 7 | disabled?: boolean; 8 | onToggle(enabled: boolean): void; 9 | /** For multiple switches all together */ 10 | index?: string; 11 | } 12 | 13 | /** 14 | * React implementation of iOS switch with improvements 15 | * from: https://www.cssscript.com/realistic-ios-switch-pure-css/ 16 | * 17 | * @param props 18 | */ 19 | 20 | export function Switcher(props: React.PropsWithChildren) { 21 | const id = `ths-input${props.index || ""}`; 22 | 23 | const beforeLabel = (props.labelPosition === "before" && props.children) || ""; 24 | 25 | const afterLabel = (props.labelPosition === "after" && props.children) || ""; 26 | 27 | return ( 28 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/Configurator/OptionsBar/style.less: -------------------------------------------------------------------------------- 1 | .options-bar { 2 | width: 100%; 3 | display: flex; 4 | justify-content: flex-end; 5 | align-items: center; 6 | box-sizing: border-box; 7 | padding: 5px 10px; 8 | box-shadow: -3px -3px 5px #2b2b2b; 9 | background-color: #333; 10 | height: 55px; 11 | position: absolute; 12 | bottom: 0; 13 | color: #fff; 14 | 15 | transition: trasform 0.5s ease-in-out 2s; 16 | transform: translateY(100%); 17 | 18 | animation-name: option-bar-show; 19 | animation-duration: 0.8s; 20 | animation-fill-mode: forwards; 21 | animation-delay: 2s; 22 | 23 | & div { 24 | border: 2px solid #e6e6e6; 25 | padding: 2px; 26 | border-radius: 50%; 27 | width: 22px; 28 | height: 22px; 29 | 30 | & svg { 31 | cursor: pointer; 32 | transition: fill 0.5s; 33 | border-radius: 50%; 34 | 35 | & > path { 36 | fill: #e6e6e6; 37 | } 38 | 39 | & > * { 40 | transition: fill 0.5s; 41 | } 42 | } 43 | 44 | &:hover > svg > path { 45 | fill: darken(#e6e6e6, 25%); 46 | } 47 | } 48 | } 49 | 50 | @keyframes option-bar-show { 51 | from { 52 | transform: translateY(100%); 53 | } 54 | to { 55 | transform: translateY(0%); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Pass/layouts/BoardingPass.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from ".."; 3 | import { PassHeader } from "./sections/Header"; 4 | import { TravelPrimaryFields } from "./sections/PrimaryFields"; 5 | import FieldsRow from "./sections/FieldRow"; 6 | import Footer from "./sections/Footer"; 7 | import Barcode from "./components/Barcodes"; 8 | 9 | export default function BoardingPass(props: PassMixedProps) { 10 | const { 11 | secondaryFields = [], 12 | primaryFields = [], 13 | headerFields = [], 14 | auxiliaryFields = [], 15 | barcode, 16 | transitType, 17 | logo, 18 | logoText, 19 | footerImage, 20 | icon, 21 | } = props; 22 | 23 | return ( 24 | <> 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/Configurator/ModalBase/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../App/common.less"; 2 | 3 | @fullscreen-size: 650px; 4 | 5 | .modal { 6 | z-index: 999; 7 | 8 | .pagetransition(); 9 | 10 | & svg#closeIcon { 11 | position: absolute; 12 | right: 16px; 13 | top: 16px; 14 | fill: #e6e6e6; 15 | cursor: pointer; 16 | pointer-events: none; /* This icon should be placed over*/ 17 | z-index: 999; 18 | width: 22px; 19 | height: 22px; 20 | 21 | @media screen and (max-width: @fullscreen-size) { 22 | top: 10px; 23 | right: 10px; 24 | } 25 | } 26 | 27 | & > .close-underlay { 28 | background-color: rgba(33, 33, 33, 0.5); 29 | position: fixed; 30 | top: 0; 31 | right: 0; 32 | left: 0; 33 | bottom: 0; 34 | cursor: pointer; 35 | box-shadow: inset 0 0px 3000px 20px #000; 36 | filter: blur(10px); 37 | 38 | .lookMumIamAppearing(0.5s, ease-in-out); 39 | } 40 | 41 | & > .modal-content { 42 | position: absolute; 43 | z-index: 999; 44 | display: flex; 45 | color: #e6e6e6; 46 | 47 | @media screen and (max-width: @fullscreen-size) { 48 | top: 40px !important; // For close icon 49 | left: 20px !important; 50 | right: 40px !important; // For close icon 51 | bottom: 20px !important; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../App/common.less"; 2 | 3 | #pages-navigator { 4 | width: 100%; 5 | height: 100%; 6 | transition: transform 0.5s ease-in-out; 7 | display: flex; 8 | .lookMumIamAppearing(0.5s, ease-in-out, 2s); 9 | 10 | & > .page { 11 | width: 100%; 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | flex-shrink: 0; 16 | 17 | & > .export-btn { 18 | display: flex; 19 | cursor: pointer; 20 | flex-grow: 0; 21 | flex-shrink: 0; 22 | justify-content: space-between; 23 | align-items: center; 24 | padding: 0 15px; 25 | min-height: 55px; 26 | transition: border-left 0.5s ease-in-out, border-bottom 0.5s ease-in-out; 27 | border-left: 0px solid #444; 28 | 29 | &.disabled { 30 | pointer-events: none; 31 | 32 | & h3 { 33 | color: #464646; 34 | } 35 | 36 | & svg { 37 | fill: #464646; 38 | } 39 | } 40 | 41 | & > h3 { 42 | font-weight: 300; 43 | color: #d2d2d2; 44 | margin: 0; 45 | transition: color 0.5s ease-in-out; 46 | } 47 | 48 | & .icon { 49 | width: 21px; 50 | height: 21px; 51 | fill: #d2d2d2; 52 | transition: fill 0.5s ease-in-out; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /partners-templates/passkit-generator.hbs: -------------------------------------------------------------------------------- 1 | import { createPass } from "passkit-generator"; 2 | 3 | try { 4 | const examplePass = await createPass({ 5 | model: "path/to/exported/model/{{#if store.projectOptions.title}}{{ store.projectOptions.title }}{{else}}Untitled Project{{/if}}.pkpass", 6 | certificates: { 7 | wwdr: "./certs/wwdr.pem", 8 | signerCert: "./certs/signercert.pem", 9 | signerKey: { 10 | keyFile: "./certs/signerkey.pem", 11 | passphrase: "123456" 12 | } 13 | }, 14 | }); 15 | 16 | {{#each store.translations}} 17 | {{#unless (isDefaultLanguage @key)}} 18 | examplePass.localize("{{@key}}"{{#if (hasContent this.translations) }}, { 19 | /** 20 | * These translations will add-up to the ones inside {{@key}}.lproj/pass.strings. 21 | * These are actually here just as example, but they are the same inside pass.strings 22 | */ 23 | {{#each this.translations}} 24 | {{! placeholder : value}} 25 | {{this.[0]}}: "{{this.[1]}}", 26 | {{/each}} 27 | }{{~/if}}); 28 | {{/unless}} 29 | {{/each}} 30 | 31 | // Generate the stream, which gets returned through a Promise 32 | const stream: Stream = examplePass.generate(); 33 | 34 | doSomethingWithTheStream(stream); 35 | } catch (err) { 36 | doSomethingWithTheError(err); 37 | } 38 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/layouts/BoardingPass.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Layouts, PassMixedProps, Constants } from "@pkvd/pass"; 3 | import useAlternativesRegistration from "../useAlternativesRegistration"; 4 | import { PassKind } from "../../../model"; 5 | 6 | const { PKTransitType } = Constants; 7 | 8 | /** 9 | * Layout proxy with alternatives registration capability. 10 | * 11 | * @param props 12 | * @returns 13 | */ 14 | 15 | export default function BoardingPass(props: PassMixedProps) { 16 | useAlternativesRegistration( 17 | PassKind.BOARDING_PASS, 18 | { 19 | name: "Generic Boarding Pass", 20 | specificProps: { 21 | transitType: PKTransitType.Generic, 22 | }, 23 | }, 24 | { 25 | name: "Air Boarding Pass", 26 | specificProps: { 27 | transitType: PKTransitType.Air, 28 | }, 29 | }, 30 | { 31 | name: "Boat Boarding Pass", 32 | specificProps: { 33 | transitType: PKTransitType.Boat, 34 | }, 35 | }, 36 | { 37 | name: "Bus Boarding Pass", 38 | specificProps: { 39 | transitType: PKTransitType.Bus, 40 | }, 41 | }, 42 | { 43 | name: "Train Boarding Pass", 44 | specificProps: { 45 | transitType: PKTransitType.Train, 46 | }, 47 | } 48 | ); 49 | 50 | return ; 51 | } 52 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldOptionsBar/FieldOrderHandler/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import FieldsArrowIcon from "../../../../icons"; 4 | 5 | export const enum Directions { 6 | UP, 7 | DOWN, 8 | BOTH, 9 | NONE, 10 | } 11 | 12 | interface Props { 13 | allowedDirections: Directions; 14 | requestFieldOrderChange(of: number): void; 15 | } 16 | 17 | export default function FieldOrderHandler(props: Props) { 18 | const canMoveUp = canMoveInDirection(props.allowedDirections, Directions.UP); 19 | const canMoveDown = canMoveInDirection(props.allowedDirections, Directions.DOWN); 20 | 21 | return ( 22 |
23 | canMoveUp && props.requestFieldOrderChange(-1)} 26 | /> 27 | canMoveDown && props.requestFieldOrderChange(1)} 30 | /> 31 |
32 | ); 33 | } 34 | 35 | function canMoveInDirection(directionProp: Directions, targetDirection: Directions) { 36 | return ( 37 | directionProp !== Directions.NONE && 38 | (directionProp === targetDirection || directionProp === Directions.BOTH) 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/navigation.utils/navigable.hoc.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import usePageRelation from "./usePageRelation.hook"; 3 | 4 | export interface NavigableProps { 5 | pagesStatuses: boolean[]; 6 | openPage(index: number): void; 7 | closePage(index: number): void; 8 | } 9 | 10 | /** 11 | * An HOC to create relations between pages 12 | * 13 | * @param Element Main element 14 | * @param args elements linked. Just for reference when reading the code. We could have had used also an array of strings, potentially. 15 | */ 16 | 17 | export default function navigable>( 18 | Element: React.ComponentType, 19 | ...args: React.ComponentType[] 20 | ) { 21 | return function (props: T) { 22 | const controllers = args.map(usePageRelation); 23 | 24 | const openPage = React.useCallback((index: number) => { 25 | const [, open] = controllers[index]; 26 | open(); 27 | }, []); 28 | 29 | const closePage = React.useCallback((index: number) => { 30 | const [, , close] = controllers[index]; 31 | close(); 32 | }, []); 33 | 34 | return ( 35 | <> 36 | controller[0])} 39 | openPage={openPage} 40 | closePage={closePage} 41 | /> 42 | 43 | ); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Thumbnail/style.less: -------------------------------------------------------------------------------- 1 | @maxHeight: 100px; 2 | 3 | .thumbnail-primaryFields { 4 | display: flex; 5 | width: 100%; 6 | min-height: 80px; 7 | max-height: @maxHeight; 8 | margin-bottom: 15px; 9 | 10 | & .left { 11 | display: flex; 12 | flex-direction: column; 13 | flex-grow: 1; 14 | flex-shrink: 1; 15 | margin-right: 10px; 16 | 17 | & > *:first-child { 18 | flex-grow: 2; 19 | 20 | &.empty-field { 21 | border-top-left-radius: 2px; 22 | border-bottom-left-radius: 2px; 23 | } 24 | } 25 | 26 | & > *:not(:first-child) { 27 | margin-top: 10px; 28 | flex-shrink: 2; 29 | flex-wrap: wrap; 30 | overflow-y: hidden; 31 | } 32 | 33 | & > .fields-row { 34 | & > .field { 35 | margin-right: 10px; 36 | margin-bottom: 1px; 37 | } 38 | } 39 | 40 | & > .field { 41 | & > .label { 42 | font-weight: 500; 43 | } 44 | 45 | & > .value { 46 | font-size: 10pt; 47 | } 48 | } 49 | } 50 | 51 | & > *:not(.left) { 52 | flex-grow: 0; 53 | flex-shrink: 0; 54 | width: 65px; 55 | 56 | &.empty-field { 57 | border-top-right-radius: 2px; 58 | border-bottom-right-radius: 2px; 59 | } 60 | } 61 | 62 | & > .image-field { 63 | & > img { 64 | height: 100%; 65 | width: 100%; 66 | object-fit: contain; 67 | object-position: top; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/TextPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { SharedPanelProps } from ".."; 4 | import { FieldKind } from "../../../../../../model"; 5 | import useContentSavingHandler from "../useContentSavingHandler"; 6 | import CapitalHeaderTitle from "../../../components/CapitalHeaderTitle"; 7 | import CommittableTextInput from "../../../../../CommittableTextInput"; 8 | 9 | interface TextPanelProps extends SharedPanelProps { 10 | value?: string; 11 | } 12 | 13 | export default function TextPanel(props: TextPanelProps) { 14 | const [content, onContentSave] = useContentSavingHandler( 15 | props.onValueChange, 16 | props.name, 17 | props.value 18 | ); 19 | const inputRef = React.useRef(); 20 | 21 | const required = (props.data.required && ) || null; 22 | 23 | if (props.isSelected) { 24 | inputRef.current?.focus(); 25 | } 26 | 27 | return ( 28 |
29 | 33 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/Configurator/Viewer/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../App/common.less"; 2 | 3 | .viewer { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-grow: 1; 8 | width: 100%; 9 | cursor: pointer; 10 | 11 | & > .pass { 12 | cursor: initial; 13 | 14 | & .card { 15 | box-shadow: 0px 3px 10px #212121; 16 | } 17 | 18 | &:hover { 19 | box-shadow: 0px 8px 10px 3px #212121; 20 | } 21 | } 22 | 23 | &.no-empty { 24 | & > .pass .empty-field { 25 | background-color: initial; 26 | pointer-events: none; 27 | } 28 | } 29 | 30 | & > .project-title-box { 31 | position: absolute; 32 | top: 30px; 33 | left: 30px; 34 | width: 200px; 35 | .lookMumIamAppearing(0.5s, ease-in-out, 2s); 36 | 37 | & input { 38 | width: 100%; 39 | font-size: 1.3em; 40 | background-color: transparent; 41 | border: none; 42 | outline: none; 43 | padding: 5px; 44 | color: #e6e6e6; 45 | box-sizing: border-box; 46 | 47 | &::selection { 48 | background-color: #1c1c1c; 49 | color: #e6e6e6; 50 | } 51 | } 52 | 53 | &::before { 54 | border-bottom: 0.5px solid #525252; 55 | width: 0px; 56 | content: ""; 57 | position: absolute; 58 | height: 100%; 59 | pointer-events: none; 60 | transition: width 0.5s ease-in-out; 61 | } 62 | 63 | &:focus-within::before { 64 | width: 100%; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerPlaceholder/style.less: -------------------------------------------------------------------------------- 1 | #fields-placeholder { 2 | display: flex; 3 | flex-direction: column; 4 | flex-grow: 1; 5 | align-items: center; 6 | justify-content: center; 7 | 8 | & p { 9 | font-size: 13px; 10 | margin: 0; 11 | // Yeah, gray as f**k :D 12 | color: #afafaf; 13 | } 14 | 15 | & svg.icon { 16 | font-size: 30pt; 17 | margin-bottom: 10px; 18 | 19 | & > text { 20 | fill: none; 21 | stroke: #5d5d5d; 22 | stroke-width: 0.4px; 23 | stroke-linecap: round; 24 | stroke-linejoin: round; 25 | animation: neonText 7s step-end 1s forwards infinite; 26 | } 27 | } 28 | } 29 | 30 | @keyframes neonText { 31 | 0% { 32 | stroke: #e6e6e6; 33 | } 34 | 10% { 35 | stroke: #5d5d5d; 36 | } 37 | 13% { 38 | stroke: #e6e6e6; 39 | } 40 | 41 | 20% { 42 | stroke: #5d5d5d; 43 | } 44 | 22% { 45 | stroke: #e6e6e6; 46 | } 47 | 24% { 48 | stroke: #5d5d5d; 49 | } 50 | 26% { 51 | stroke: #e6e6e6; 52 | } 53 | 35% { 54 | stroke: #5d5d5d; 55 | } 56 | 57 | 50% { 58 | stroke: #e6e6e6; 59 | } 60 | 52% { 61 | stroke: #5d5d5d; 62 | } 63 | 54% { 64 | stroke: #e6e6e6; 65 | } 66 | 56% { 67 | stroke: #5d5d5d; 68 | } 69 | 65% { 70 | stroke: #e6e6e6; 71 | } 72 | 70% { 73 | stroke: #5d5d5d; 74 | } 75 | 76 | 75% { 77 | stroke: #e6e6e6; 78 | } 79 | 90% { 80 | stroke: #5d5d5d; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/PassSelector/SelectablePass/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Pass, { Layouts, PassMixedProps } from "@pkvd/pass"; 3 | import { PassKind } from "../../model"; 4 | import * as SelectableLayouts from "./layouts"; 5 | 6 | /** 7 | * This module defines an alternative sets of layouts, which 8 | * are the same layouts but with additional features just for 9 | * selection, to keep the main layout component as clean as 10 | * possible. 11 | * 12 | * Also, it allows defining a name for the pass to be showed 13 | * under it as a description. 14 | */ 15 | 16 | export interface SelectablePassProps extends PassMixedProps { 17 | name: string; 18 | } 19 | 20 | const LayoutsMap = new Map([ 21 | [PassKind.BOARDING_PASS, SelectableLayouts.BoardingPass], 22 | [PassKind.COUPON, SelectableLayouts.Coupon], 23 | [PassKind.EVENT, SelectableLayouts.EventTicket], 24 | [PassKind.GENERIC, SelectableLayouts.Generic], 25 | [PassKind.STORE, SelectableLayouts.StoreCard], 26 | ]); 27 | 28 | export default function SelectablePass(props: SelectablePassProps): JSX.Element { 29 | const { name, ...passProps } = props; 30 | 31 | const PassLayout = LayoutsMap.get(passProps.kind); 32 | 33 | return ( 34 | <> 35 |
36 | 37 |
38 |
{name}
39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { PassMediaProps, PassMixedProps } from "@pkvd/pass"; 4 | import PanelsPage from "./pages/PanelsPage"; 5 | import { FieldDetails } from "./pages/PanelsPage/Panel"; 6 | import type RegistrationIndex from "../RegistrationIndex"; 7 | import { usePageRelation, usePagesAmount } from "./navigation.utils"; 8 | 9 | interface Props { 10 | selectedRegistrable?: FieldDetails; 11 | fields: RegistrationIndex; 12 | data: PassMixedProps; 13 | cancelFieldSelection(): void; 14 | onValueChange(key: string, value: any): Promise; 15 | requestExport(): void; 16 | onMediaEditRequest(mediaName: keyof PassMediaProps): void; 17 | } 18 | 19 | export default function OptionsMenu(props: Props) { 20 | const pagesAmount = usePagesAmount(); 21 | const [, open] = usePageRelation(); 22 | 23 | React.useLayoutEffect(() => { 24 | // Opening PanelsPage, which is always available 25 | open(); 26 | }, []); 27 | 28 | return ( 29 |
30 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | 4 | export default function LoaderFace() { 5 | return ( 6 |
7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { useRegistrations } from "../useRegistrations"; 4 | import { FieldKind } from "../../../../model"; 5 | import ImageField, { ImageFieldProps } from "../../components/ImageField"; 6 | import { AppIconEmpty } from "./icons"; 7 | 8 | interface FooterProps extends ImageFieldProps { 9 | allowFooterImage?: boolean; 10 | icon?: string; 11 | } 12 | 13 | export default function Footer(props: React.PropsWithChildren) { 14 | const { children = null } = props; 15 | let footerImage: JSX.Element = null; 16 | 17 | const registrationsDescriptors: Parameters[0] = [ 18 | [FieldKind.IMAGE, "icon"], 19 | ]; 20 | 21 | if (props.allowFooterImage) { 22 | registrationsDescriptors.push([FieldKind.IMAGE, "footerImage"]); 23 | } 24 | 25 | const [iconClickHandler, footerImageClickHandler] = useRegistrations(registrationsDescriptors); 26 | 27 | return ( 28 |
29 |
30 |
iconClickHandler("icon")}> 31 | {props.icon ? icon : } 32 |
33 | {(footerImageClickHandler && ( 34 | footerImageClickHandler(null)} /> 35 | )) || 36 | null} 37 | {children} 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Strip/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { getFilteredFieldData } from "../../../components/Field/getFilteredFieldData"; 5 | import ImageField from "../../../components/ImageField"; 6 | import { FieldValue, FieldLabel, GhostField } from "../../../components/Field"; 7 | import { useRegistrations } from "../../useRegistrations"; 8 | import { FieldKind } from "../../../../../model"; 9 | 10 | interface PFStripProps { 11 | fields?: Constants.PassField[]; 12 | stripSrc?: string; 13 | } 14 | 15 | export default function StripPrimaryFields( 16 | props: React.PropsWithChildren 17 | ): JSX.Element { 18 | const { fields, stripSrc } = props; 19 | const [primaryFieldsClickHandler, stripImageClickHandler] = useRegistrations([ 20 | [FieldKind.FIELDS, "primaryFields"], 21 | [FieldKind.IMAGE, "stripImage"], 22 | ]); 23 | 24 | const data = getFilteredFieldData(fields, 1, 1).map((data) => { 25 | return ( 26 | 27 | 28 | 29 | 30 | ); 31 | }); 32 | 33 | return ( 34 |
35 |
{data}
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/Pass/layouts/Coupon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from ".."; 3 | import { PassHeader } from "./sections/Header"; 4 | import { StripPrimaryFields } from "./sections/PrimaryFields"; 5 | import FieldsRow from "./sections/FieldRow"; 6 | import Barcode from "./components/Barcodes"; 7 | import Footer from "./sections/Footer"; 8 | 9 | export default function Coupon(props: PassMixedProps): JSX.Element { 10 | const { 11 | secondaryFields = [], 12 | primaryFields = [], 13 | headerFields = [], 14 | auxiliaryFields = [], 15 | barcode, 16 | stripImage, 17 | logo, 18 | logoText, 19 | icon, 20 | } = props; 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 38 |
39 | 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/Pass/layouts/StoreCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from ".."; 3 | import { PassHeader } from "./sections/Header"; 4 | import { StripPrimaryFields } from "./sections/PrimaryFields"; 5 | import FieldsRow from "./sections/FieldRow"; 6 | import Footer from "./sections/Footer"; 7 | import Barcodes from "./components/Barcodes"; 8 | 9 | export default function StoreCard(props: PassMixedProps): JSX.Element { 10 | const { 11 | secondaryFields = [], 12 | primaryFields = [], 13 | headerFields = [], 14 | auxiliaryFields = [], 15 | barcode, 16 | logo, 17 | logoText, 18 | stripImage, 19 | icon, 20 | } = props; 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 38 |
39 | 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldOptionsBar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { DeleteFieldIcon, ListAddProp } from "./icons"; 4 | import FieldOrderHandler, { Directions } from "./FieldOrderHandler"; 5 | 6 | interface FieldOptionsProps { 7 | deleteField(fieldUUID: string): void; 8 | requestFieldOrderChange(fieldUUID: string, of: number): void; 9 | onPropsEditClick(): void; 10 | fieldUUID: string; 11 | isUpperBoundary: boolean; 12 | isLowerBoundary: boolean; 13 | } 14 | 15 | export default function FieldOptionsBar(props: FieldOptionsProps) { 16 | const allowedMovingDirections = 17 | props.isLowerBoundary && props.isUpperBoundary 18 | ? Directions.NONE 19 | : props.isLowerBoundary 20 | ? Directions.UP 21 | : props.isUpperBoundary 22 | ? Directions.DOWN 23 | : Directions.BOTH; 24 | 25 | return ( 26 | <> 27 |
28 |
props.deleteField(props.fieldUUID)}> 29 | 30 |
31 | 34 | props.requestFieldOrderChange(props.fieldUUID, amount) 35 | } 36 | /> 37 |
38 | 39 |
40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import FieldOptionsBar from "./FieldOptionsBar"; 5 | import FieldPreview from "../../components/FieldPreview"; 6 | 7 | type PassField = Constants.PassField; 8 | 9 | interface DrawerElementProps { 10 | onFieldDelete(key: string): void; 11 | onFieldOrderChange(fieldUUID: string, of: number): void; 12 | elementData: PassField; 13 | isUpperBoundary: boolean; 14 | isLowerBoundary: boolean; 15 | openDetailsPage(fieldUUID: string): void; 16 | } 17 | 18 | export default function DrawerElement(props: DrawerElementProps) { 19 | const onEditPropertiesHandler = React.useCallback(() => { 20 | props.openDetailsPage(props.elementData.fieldUUID); 21 | }, [props.elementData.fieldUUID]); 22 | 23 | return ( 24 |
28 | 33 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Travel/style.less: -------------------------------------------------------------------------------- 1 | .travel-primaryFields { 2 | display: grid; 3 | width: 100%; 4 | grid-template-columns: 5 | [full-start] minmax(0, 1fr) [value-end] minmax(0, 25px) [center] minmax(0, 25px) [value-start] minmax( 6 | 0, 7 | 1fr 8 | ) 9 | [full-end]; 10 | grid-template-rows: [label-line-start] minmax(0, 14px) [value-line-start] minmax(0, 35px) [lines-end]; 11 | 12 | & > .empty-field { 13 | grid-row: ~"1 / 3"; 14 | 15 | /** 16 | * If we have at least one placeholder, 17 | * we align the icon in the real grid 18 | * center and not in the bottom center 19 | */ 20 | & + .icon, 21 | & ~ .icon { 22 | grid-row: ~"1 / 3"; 23 | } 24 | 25 | &:first-child { 26 | border-top-left-radius: 2px; 27 | border-bottom-left-radius: 2px; 28 | } 29 | 30 | &:last-child { 31 | border-top-right-radius: 2px; 32 | border-bottom-right-radius: 2px; 33 | } 34 | } 35 | 36 | & > .label { 37 | grid-column: ~"auto / span 2"; 38 | grid-row: ~"1 / 1"; 39 | } 40 | 41 | & > .value { 42 | grid-column: ~"auto / span 1"; 43 | grid-row: ~"2 / 2"; 44 | 45 | line-height: 22pt; 46 | font-size: 23pt; 47 | flex: 0 0 100px; 48 | 49 | &:first-child { 50 | margin-right: 10px; 51 | } 52 | 53 | &:last-child { 54 | margin-left: 10px; 55 | text-align: end !important; 56 | } 57 | } 58 | 59 | & > .icon { 60 | grid-row: ~"2 / 2"; 61 | grid-column: ~"2 / 4"; 62 | justify-self: center; 63 | align-self: center; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/TabsList/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../../App/common.less"; 2 | 3 | .page { 4 | & > ul { 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | height: 50px; 9 | list-style-type: none; 10 | padding: 0; 11 | margin: 0; 12 | color: #e6e6e6; 13 | border-bottom: 0.5px solid rgba(230, 230, 230, 0.2); 14 | flex-shrink: 0; 15 | .scrollableWithoutScrollbars(x); 16 | 17 | &.left { 18 | &::before { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | &.right { 24 | &::after { 25 | opacity: 1; 26 | } 27 | } 28 | 29 | &::before, 30 | &::after { 31 | content: ""; 32 | position: fixed; 33 | top: 0; 34 | bottom: 0; 35 | width: 50px; 36 | height: 50px; 37 | opacity: 0; 38 | transition: opacity 0.2s ease-in-out; 39 | pointer-events: none; 40 | } 41 | 42 | &::after { 43 | right: 0; 44 | background: linear-gradient(to left, #1c1c1c 0%, transparent); 45 | } 46 | 47 | &::before { 48 | left: 0; 49 | background: linear-gradient(to right, #1c1c1c 0%, transparent); 50 | } 51 | 52 | & li { 53 | padding: 0 10px; 54 | margin: auto; 55 | cursor: pointer; 56 | display: flex; 57 | align-items: center; 58 | height: 49px; 59 | transition: border-bottom 0.2s ease-in-out; 60 | border-bottom: 0.5px solid transparent; 61 | 62 | &.active { 63 | font-weight: 400; 64 | border-bottom: 0.5px solid rgb(255, 176, 0); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Configurator/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../App/common.less"; 2 | 3 | #configurator { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | overflow: hidden; 8 | 9 | .pagetransition(); 10 | 11 | & > .screen { 12 | display: flex; 13 | flex-direction: column; 14 | flex-grow: 1; 15 | 16 | /** 17 | * This is needed to allow the appearance animation 18 | * of the absolute-positioned option bar 19 | */ 20 | position: relative; 21 | 22 | /** 23 | * This is needed to avoid bottom bar appearance to 24 | * make the page scrollable 25 | */ 26 | overflow: hidden; 27 | } 28 | 29 | & > .config-panel { 30 | display: flex; 31 | background-color: #1c1c1c; 32 | flex-shrink: 0; 33 | overflow: hidden; 34 | animation-duration: 0.8s; 35 | animation-fill-mode: forwards; 36 | animation-delay: 1s; 37 | animation-timing-function: ease-in-out; 38 | box-shadow: -3px 0 10px #2b2b2b; 39 | 40 | @media screen and (min-width: 550px) { 41 | width: 0px; 42 | animation-name: open-menu-norm; 43 | 44 | @keyframes open-menu-norm { 45 | to { 46 | width: 300px; 47 | } 48 | } 49 | } 50 | 51 | @media screen and (max-width: 550px) { 52 | width: 300px; 53 | right: -300px; 54 | position: absolute; 55 | top: 0; 56 | bottom: 0; 57 | animation-name: open-menu-sm; 58 | 59 | @keyframes open-menu-sm { 60 | to { 61 | right: 0; 62 | } 63 | } 64 | } 65 | 66 | @media screen and (max-width: 300px) { 67 | max-width: 100%; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/FieldRow/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Field, FieldLabel, FieldValue } from "../../components/Field"; 4 | import { useRegistrations } from "../useRegistrations"; 5 | import { FieldKind } from "../../../../model"; 6 | import { PassField } from "../../../constants"; 7 | import { createClassName } from "../../../../utils"; 8 | import { getFilteredFieldData } from "../../components/Field/getFilteredFieldData"; 9 | 10 | interface RowProps { 11 | id: string; 12 | maximumElementsAmount: number; 13 | elements: PassField[]; 14 | className?: string; 15 | } 16 | 17 | /** 18 | * Container for Apple Wallet Fields 19 | * 20 | * @param props 21 | */ 22 | 23 | export default function FieldsRow(props: RowProps) { 24 | const { maximumElementsAmount = 0, id, elements = [], className: externalClassName } = props; 25 | 26 | const [fieldsClickHandler] = useRegistrations([[FieldKind.FIELDS, id]]); 27 | 28 | /** Forcing one or we'd get too much fields as fallback */ 29 | const mappedElements = getFilteredFieldData(elements, 1, maximumElementsAmount).map( 30 | (data, index) => ( 31 | fieldsClickHandler(data.key ?? null)} 34 | fieldData={data} 35 | > 36 | 37 | 38 | 39 | ) 40 | ); 41 | 42 | const className = createClassName(["fields-row", externalClassName]); 43 | 44 | return
{mappedElements}
; 45 | } 46 | -------------------------------------------------------------------------------- /src/Configurator/RegistrationIndex.ts: -------------------------------------------------------------------------------- 1 | import { DataGroup } from "./OptionsMenu/pages/PanelsPage"; 2 | import { FieldDetails } from "./OptionsMenu/pages/PanelsPage/Panel"; 3 | 4 | export default class RegistrationIndex { 5 | private dataGroupMap = new Map(); 6 | private idMap = new Map(); 7 | 8 | constructor(initialIndex: Map | [DataGroup, FieldDetails[]][]) { 9 | for (let [group, details] of initialIndex) { 10 | for (let detail of details) { 11 | this.setId(detail.name, detail); 12 | this.setDatagroup(group, detail); 13 | } 14 | } 15 | } 16 | 17 | setByDatagroup(group: DataGroup, value: FieldDetails) { 18 | this.setDatagroup(group, value); 19 | this.setId(value.name, value); 20 | } 21 | 22 | setById(value: FieldDetails) { 23 | this.setId(value.name, value); 24 | this.setDatagroup(value.group, value); 25 | } 26 | 27 | getById(id: string) { 28 | return this.idMap.get(id) || null; 29 | } 30 | 31 | getDatagroup(group: DataGroup) { 32 | return this.dataGroupMap.get(group); 33 | } 34 | 35 | findInDatagroup(group: DataGroup, id: string) { 36 | const dataGroup = this.dataGroupMap.get(group); 37 | 38 | return dataGroup?.find(details => details.name === id); 39 | } 40 | 41 | private setDatagroup(group: DataGroup, value: FieldDetails) { 42 | this.dataGroupMap.set(group, [ 43 | ...(this.dataGroupMap.get(group) || []), 44 | value 45 | ]); 46 | } 47 | 48 | private setId(id: string, value: FieldDetails) { 49 | this.idMap.set(id, value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Configurator/TranslationsModal/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const DeleteIcon = (props: React.SVGProps) => ( 4 | 5 | 6 | 7 | 8 | ); 9 | 10 | // add by Harper from the Noun Project 11 | // https://thenounproject.com/term/add/1623623 12 | 13 | export const AddIcon = (props: React.SVGProps) => ( 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /src/Pass/layouts/Generic.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from ".."; 3 | import { PassHeader } from "./sections/Header"; 4 | import { ThumbnailPrimaryFields } from "./sections/PrimaryFields"; 5 | import Barcodes, { isSquareBarcode } from "./components/Barcodes"; 6 | import FieldsRow from "./sections/FieldRow"; 7 | import Footer from "./sections/Footer"; 8 | 9 | export default function Generic(props: PassMixedProps): JSX.Element { 10 | const { 11 | secondaryFields = [], 12 | primaryFields = [], 13 | headerFields = [], 14 | auxiliaryFields = [], 15 | barcode, 16 | logoText, 17 | logo, 18 | thumbnailImage, 19 | icon, 20 | } = props; 21 | 22 | const isSquaredBarcode = isSquareBarcode(barcode?.format); 23 | 24 | const middleFragment = (isSquaredBarcode && ( 25 | 30 | )) || ( 31 | <> 32 | 33 | 34 | 35 | ); 36 | 37 | return ( 38 | <> 39 | 40 | 41 | {middleFragment} 42 |
43 | 44 |
45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/FieldPropertiesEditList/FieldPropertyPanels/Enum.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | name: string; 5 | value?: string; 6 | options: E; 7 | defaultValue?: string; 8 | onValueChange(prop: string, value: T): void; 9 | } 10 | 11 | export default function FieldEnumPropertyPanel(props: Props) { 12 | const [selectedValue, changeSelectedValue] = React.useState( 13 | props.value || props.defaultValue || null 14 | ); 15 | 16 | const isDefaultValueAnOption = Boolean( 17 | props.defaultValue && Object.values(props.options).includes(props.defaultValue) 18 | ); 19 | 20 | React.useEffect(() => { 21 | if (selectedValue !== (props.value || props.defaultValue)) { 22 | props.onValueChange(props.name, selectedValue); 23 | } 24 | }, [selectedValue]); 25 | 26 | const options = [ 27 | (!isDefaultValueAnOption && ( 28 | 31 | )) || 32 | null, 33 | ...Object.entries(props.options).map(([key, object]) => ( 34 | 37 | )), 38 | ]; 39 | 40 | return ( 41 |
42 | 43 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/Footer/style.less: -------------------------------------------------------------------------------- 1 | @footer-picture-row-height: 18px; 2 | @footer-picture-row-margins: 10px 0 5px 0; 3 | 4 | .footer { 5 | display: flex; 6 | align-items: flex-end; 7 | justify-content: center; 8 | width: 100%; 9 | flex-grow: 1; 10 | 11 | & > div.grid { 12 | display: grid; 13 | width: 100%; 14 | row-gap: 5px; 15 | grid-template-columns: 25px 1fr 25px; 16 | grid-template-rows: 20px 1fr; 17 | position: absolute; 18 | 19 | & > .icon { 20 | grid-column: ~"1 / 2"; 21 | grid-row: ~"2 / 3"; 22 | 23 | background-color: #cecece; 24 | width: 15px; 25 | height: 15px; 26 | 27 | margin-left: 5px; 28 | margin-bottom: -5px; 29 | 30 | align-self: flex-end; 31 | border-radius: 4px; 32 | cursor: pointer; 33 | 34 | & > svg { 35 | & .cls-1 { 36 | fill: #5a5a5aa6; 37 | } 38 | 39 | & .cls-2 { 40 | fill: none; 41 | stroke: #5a5a5aa6; 42 | stroke-width: 0.2px; 43 | } 44 | } 45 | 46 | & > img { 47 | border-radius: 4px; 48 | } 49 | } 50 | } 51 | 52 | & .empty-field { 53 | width: 100%; 54 | height: @footer-picture-row-height; 55 | flex-grow: 0; 56 | border-radius: 2px; 57 | 58 | grid-column: ~"2 / 3"; 59 | grid-row: ~"1 / 2"; 60 | } 61 | 62 | & .image-field { 63 | justify-self: center; 64 | align-self: center; 65 | 66 | display: flex; 67 | justify-content: center; 68 | 69 | grid-column: ~"2 / 3"; 70 | grid-row: ~"1 / 2"; 71 | 72 | /* For picture prop object-fit below */ 73 | height: 100%; 74 | 75 | & > img { 76 | object-fit: scale-down; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Travel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { getFilteredFieldData } from "../../../components/Field/getFilteredFieldData"; 5 | import { GhostField, FieldLabel, FieldValue } from "../../../components/Field"; 6 | import { PKTransitType } from "../../../../constants"; 7 | import { PKTransitIcon } from "./icons"; 8 | import { useRegistrations } from "../../useRegistrations"; 9 | import { FieldKind } from "../../../../../model"; 10 | 11 | interface PFTravelProps { 12 | fields?: Constants.PassField[]; 13 | transitType: PKTransitType; 14 | } 15 | 16 | export default function PrimaryFields(props: PFTravelProps) { 17 | const { fields, transitType } = props; 18 | const parentId = "primaryFields"; 19 | 20 | const [primaryFieldsClickHandler] = useRegistrations([[FieldKind.FIELDS, parentId]]); 21 | 22 | const [from, to] = getFilteredFieldData(fields, 2, 2).map((fieldData, index) => { 23 | const id = `${parentId}.${index}`; 24 | 25 | return ( 26 | primaryFieldsClickHandler(fieldData?.key ?? null)} 29 | fieldData={fieldData} 30 | > 31 | 32 | 33 | 34 | ); 35 | }); 36 | 37 | return ( 38 |
39 | {from} 40 | 45 | {to} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/fieldCommons.ts: -------------------------------------------------------------------------------- 1 | import { PKTextAlignment, PassField } from "../../../constants"; 2 | import { StylingProps } from "../../../../model"; 3 | 4 | export function getCSSFromFieldProps( 5 | props: Partial, 6 | origin: "label" | "value" 7 | ): React.CSSProperties { 8 | const textAlignment = props.textAlignment || PKTextAlignment.Natural; 9 | 10 | return { 11 | textAlign: transformPKTextAlignmentToCSS(textAlignment), 12 | color: String((origin === "value" && props.textColor) || props.labelColor) || "#000", 13 | overflow: "hidden", 14 | textOverflow: "ellipsis", 15 | }; 16 | } 17 | 18 | function transformPKTextAlignmentToCSS(textAlignment: PKTextAlignment) { 19 | switch (textAlignment) { 20 | case PKTextAlignment.Left: 21 | return "left"; 22 | case PKTextAlignment.Center: 23 | return "center"; 24 | case PKTextAlignment.Right: 25 | return "right"; 26 | case PKTextAlignment.Natural: 27 | return "start"; 28 | } 29 | } 30 | 31 | type LabelSpecificProps = { 32 | labelColor?: string; 33 | label?: string; 34 | }; 35 | 36 | type ValueSpecificProps = { 37 | value: any; 38 | textColor?: string; 39 | }; 40 | 41 | export const enum FieldTypes { 42 | LABEL, 43 | VALUE, 44 | BOTH, 45 | } 46 | 47 | export type FieldProperties = Omit< 48 | PassField, 49 | "value" | "label" 50 | > & 51 | StylingProps & 52 | (T extends FieldTypes.LABEL 53 | ? LabelSpecificProps 54 | : T extends FieldTypes.VALUE 55 | ? ValueSpecificProps 56 | : T extends FieldTypes.BOTH 57 | ? LabelSpecificProps & ValueSpecificProps 58 | : never); 59 | -------------------------------------------------------------------------------- /src/Loader/style.less: -------------------------------------------------------------------------------- 1 | #loader-face { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background-color: #272727; 8 | z-index: 10; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | animation-duration: 0.5s; 13 | animation-play-state: paused; 14 | animation-fill-mode: forwards; 15 | animation-timing-function: ease-out; 16 | 17 | // CSS Transition 18 | &.enter { 19 | opacity: 0; 20 | animation-name: loader-appear; 21 | animation-play-state: running; 22 | } 23 | 24 | &.exit { 25 | opacity: 1; 26 | animation-name: loader-disappear; 27 | animation-play-state: running; 28 | } 29 | 30 | & svg { 31 | & #content > * { 32 | fill: #ededed; 33 | opacity: 0.3; 34 | animation-name: bouncing-opacity; 35 | animation-duration: 0.7s; 36 | animation-iteration-count: infinite; 37 | animation-direction: alternate; 38 | animation-timing-function: ease-in-out; 39 | } 40 | 41 | & #border { 42 | stroke: #ededed; 43 | stroke-width: 5; 44 | stroke-linecap: round; 45 | stroke-dasharray: 960 40; 46 | stroke-dashoffset: 1000; 47 | 48 | animation-name: rotation-border; 49 | animation-duration: 2s; 50 | animation-iteration-count: infinite; 51 | animation-timing-function: linear; 52 | } 53 | } 54 | } 55 | 56 | @keyframes bouncing-opacity { 57 | to { 58 | opacity: 1; 59 | } 60 | } 61 | 62 | @keyframes rotation-border { 63 | to { 64 | stroke-dashoffset: 0; 65 | } 66 | } 67 | 68 | @keyframes loader-disappear { 69 | to { 70 | opacity: 0; 71 | } 72 | } 73 | 74 | @keyframes loader-appear { 75 | to { 76 | opacity: 1; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/style.less: -------------------------------------------------------------------------------- 1 | #pages-navigator > .page > .fields-preview-page { 2 | display: flex; 3 | flex-direction: column; 4 | flex-grow: 1; 5 | color: #e6e6e6; 6 | opacity: 0.3; 7 | transform: scale(0.95); 8 | animation: appear 0.5s ease-in-out 0.1s forwards; 9 | /* To scroll flexbox along with its props */ 10 | overflow: auto; 11 | position: relative; 12 | 13 | & .more-items-indicator { 14 | position: absolute; 15 | bottom: 15px; 16 | left: 0; 17 | right: 0; 18 | display: flex; 19 | justify-content: center; 20 | opacity: 0; 21 | transition: 0.3s ease-in-out; 22 | pointer-events: none; 23 | 24 | & svg { 25 | height: 20px; 26 | width: 50px; 27 | 28 | & circle { 29 | fill: #fff; 30 | } 31 | } 32 | } 33 | 34 | & footer { 35 | height: 15px; 36 | border-top: 1px solid #333; 37 | padding: 20px 10px; 38 | display: flex; 39 | align-items: center; 40 | justify-content: flex-end; 41 | 42 | & button { 43 | background-color: #333; 44 | border: 0; 45 | color: #e6e6e6; 46 | padding: 4px; 47 | transition: background-color 0.2s ease-in-out; 48 | cursor: pointer; 49 | 50 | &.json-mode-active { 51 | background-color: #003967; 52 | } 53 | } 54 | } 55 | } 56 | 57 | svg.add { 58 | width: 28px; 59 | fill: #333333; 60 | cursor: pointer; 61 | 62 | &:active path:first-child { 63 | fill: #252525; 64 | } 65 | 66 | & path:nth-child(2) { 67 | fill: #e6e6e6; 68 | } 69 | } 70 | 71 | @keyframes appear { 72 | from { 73 | transform: scale(0.95); 74 | opacity: 0.3; 75 | } 76 | 77 | to { 78 | transform: scale(1); 79 | opacity: 1; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/useRegistrations.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { InteractionContext, PassMixedProps } from "../.."; 3 | import { FieldKind } from "../../../model"; 4 | 5 | // I actually not really understood how does conditional distributed types work... 6 | // But what I wanted to achieve is to obtain a "forced" no-parameter function 7 | // If a SelectableComponent does not return 8 | export type FieldSelectHandler

= [P] extends [never] 9 | ? () => void 10 | : (fieldIdentifier: string | null) => void; 11 | export type onRegister = (kind: FieldKind, id: keyof PassMixedProps | string) => FieldSelectHandler; 12 | 13 | export interface SelectableComponent

{ 14 | onClick: FieldSelectHandler

; 15 | } 16 | 17 | type RegistrationDescriptor = [kind: FieldKind, fieldName: string]; 18 | 19 | const noop = () => {}; 20 | 21 | /** 22 | * Registration principle regards having a way to click on 23 | * an element and trigger something in a parent, which uses 24 | * the InteractionContext provided by the Pass component. 25 | * 26 | * So, the context defines the registration function and 27 | * the registration function, once invoked, returns a 28 | * click handler. 29 | * 30 | * @param components 31 | * @returns 32 | */ 33 | 34 | export function useRegistrations( 35 | components: RegistrationDescriptor[] 36 | ): FieldSelectHandler[] { 37 | const registerField = React.useContext(InteractionContext); 38 | 39 | return React.useMemo(() => { 40 | if (!registerField) { 41 | return components.map(() => noop); 42 | } 43 | 44 | return components.map(([kind, id]) => registerField(kind, id)); 45 | }, [registerField]); 46 | } 47 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import TextField from "../../components/TextField"; 4 | import { useRegistrations } from "../useRegistrations"; 5 | import ImageField from "../../components/ImageField"; 6 | import FieldsRow from "../FieldRow"; 7 | import { FieldKind } from "../../../../model"; 8 | import { PassField } from "../../../constants"; 9 | 10 | interface HeaderProps { 11 | headerFields?: PassField[]; 12 | logoText?: string; 13 | logo?: string; 14 | } 15 | 16 | export function PassHeader(props: HeaderProps) { 17 | /** 18 | * The Field row will register itself 19 | * with the ID we pass to it. 20 | */ 21 | const [logoClickHandler, logoTextClickHandler] = useRegistrations([ 22 | [FieldKind.IMAGE, "logo"], 23 | [FieldKind.TEXT, "logoText"], 24 | ]); 25 | 26 | /** 27 | * This is to make fallback growing and be visible 28 | * We need to have at least one element that have value or label 29 | */ 30 | const canGrowRowCN = 31 | (!( 32 | props.headerFields.length && props.headerFields.some((field) => field.value || field.label) 33 | ) && 34 | "can-grow") || 35 | ""; 36 | 37 | return ( 38 |

39 |
40 | logoClickHandler(null)} /> 41 | logoTextClickHandler(null)} 45 | /> 46 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/PrimaryFields/Thumbnail/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { getFilteredFieldData } from "../../../components/Field/getFilteredFieldData"; 5 | import { Field, FieldLabel, FieldValue } from "../../../components/Field"; 6 | import ImageField from "../../../components/ImageField"; 7 | import { useRegistrations } from "../../useRegistrations"; 8 | import { FieldKind } from "../../../../../model"; 9 | 10 | interface PFThumbnailProps { 11 | fields?: Constants.PassField[]; 12 | thumbnailSrc?: string; 13 | } 14 | 15 | export default function ThumbnailPrimaryField(props: React.PropsWithChildren) { 16 | const { fields, thumbnailSrc, children } = props; 17 | const parentId = "primaryFields"; 18 | 19 | const [primaryFieldsClickHandler, thumbnailClickHandler] = useRegistrations([ 20 | [FieldKind.FIELDS, parentId], 21 | [FieldKind.IMAGE, "thumbnailImage"], 22 | ]); 23 | 24 | const data = getFilteredFieldData(fields, 1, 1).map((fieldData, index) => { 25 | const key = `${parentId}.${index}`; 26 | 27 | return ( 28 | primaryFieldsClickHandler(fieldData?.key ?? null)} 31 | fieldData={fieldData} 32 | > 33 | 34 | 35 | 36 | ); 37 | }); 38 | 39 | return ( 40 |
41 |
42 | {data} 43 | {children} 44 |
45 |
46 | thumbnailClickHandler(null)} 51 | /> 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldPropertiesDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Constants } from "@pkvd/pass"; 2 | 3 | const { PKTextAlignment, PKDateStyle, PKDataDetectorType } = Constants; 4 | type PassField = Constants.PassField; 5 | 6 | type FieldPropertyDetail = { 7 | name: keyof PassField; 8 | type: 9 | | typeof String 10 | | typeof Boolean 11 | | typeof PKTextAlignment 12 | | typeof PKDateStyle 13 | | typeof PKDataDetectorType; 14 | placeholder?: string; 15 | optional?: boolean; 16 | defaultValue?: string; 17 | }; 18 | 19 | export const FieldPropertiesDetails: FieldPropertyDetail[] = [ 20 | { 21 | name: "value", 22 | type: String, 23 | optional: false, 24 | }, 25 | { 26 | name: "label", 27 | type: String, 28 | optional: true, 29 | }, 30 | { 31 | name: "attributedValue", 32 | type: String, 33 | placeholder: "Edit my profile", 34 | optional: true, 35 | }, 36 | { 37 | name: "changeMessage", 38 | type: String, 39 | placeholder: "Gate changed to %@", 40 | optional: true, 41 | }, 42 | { 43 | name: "dataDetectorTypes", 44 | type: PKDataDetectorType, 45 | optional: true, 46 | defaultValue: "None", 47 | }, 48 | { 49 | name: "textAlignment", 50 | type: PKTextAlignment, 51 | optional: true, 52 | defaultValue: PKTextAlignment.Natural, 53 | }, 54 | { 55 | name: "dateStyle", 56 | type: PKDateStyle, 57 | optional: true, 58 | defaultValue: PKDateStyle.None, 59 | }, 60 | { 61 | name: "timeStyle", 62 | type: PKDateStyle, 63 | optional: true, 64 | defaultValue: PKDateStyle.None, 65 | }, 66 | { 67 | name: "ignoresTimeZone", 68 | type: Boolean, 69 | optional: true, 70 | }, 71 | { 72 | name: "isRelative", 73 | type: Boolean, 74 | optional: true, 75 | }, 76 | ]; 77 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/Footer/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function AppIconEmpty(props: React.SVGProps) { 4 | return ( 5 | 6 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/Configurator/CommittableTextInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props extends Omit, "type"> { 4 | selectOnFocus?: boolean; 5 | commit(content: string): void; 6 | } 7 | 8 | export default React.forwardRef(function CommittableTextInput( 9 | props: Props, 10 | ref: React.RefObject 11 | ) { 12 | const { 13 | commit, 14 | selectOnFocus, 15 | onFocus, 16 | onFocusCapture, 17 | onBlur, 18 | onBlurCapture, 19 | onKeyDown, 20 | onKeyDownCapture, 21 | ...inputProps 22 | } = props; 23 | 24 | const onFocusHandler = React.useCallback( 25 | (event: React.FocusEvent) => { 26 | if (selectOnFocus) { 27 | // To select all the text in the input - figma style 28 | event.currentTarget.select(); 29 | } 30 | 31 | onFocus?.(event); 32 | onFocusCapture?.(event); 33 | }, 34 | [selectOnFocus, onFocus, onFocusCapture] 35 | ); 36 | 37 | const onKeyDownHandler = React.useCallback( 38 | (event: React.KeyboardEvent) => { 39 | const { key, currentTarget } = event; 40 | 41 | if (key === "Enter") { 42 | currentTarget.blur(); 43 | } 44 | 45 | onKeyDown?.(event); 46 | onKeyDownCapture?.(event); 47 | }, 48 | [onKeyDown, onKeyDownCapture] 49 | ); 50 | 51 | const onBlurHandler = React.useCallback( 52 | (event: React.FocusEvent) => { 53 | const { value } = event.currentTarget; 54 | commit(value || undefined); 55 | 56 | onBlur?.(event); 57 | onBlurCapture?.(event); 58 | }, 59 | [commit, onBlur, onBlurCapture] 60 | ); 61 | 62 | return ( 63 | 71 | ); 72 | }); 73 | -------------------------------------------------------------------------------- /src/store/forage.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | import { State } from "."; 3 | 4 | export interface ForageStructure { 5 | projects: { 6 | [projectName: string]: { 7 | preview: ArrayBuffer; 8 | snapshot: State; 9 | }; 10 | }; 11 | } 12 | 13 | // ************************************************************************ // 14 | 15 | // ********************* // 16 | // *** ACTION TYPES *** // 17 | // ********************* // 18 | 19 | // ************************************************************************ // 20 | 21 | export const INIT = "forage/INIT"; 22 | export const RESET = "forage/RESET"; 23 | 24 | // ************************************************************************ // 25 | 26 | // ********************* // 27 | // *** MEDIA REDUCER *** // 28 | // ********************* // 29 | 30 | // ************************************************************************ // 31 | 32 | /* NONE */ 33 | 34 | // ************************************************************************ // 35 | 36 | // *********************** // 37 | // *** ACTION CREATORS *** // 38 | // *********************** // 39 | 40 | // ************************************************************************ // 41 | 42 | export function Init(snapshot: State): Actions.Init { 43 | return { 44 | type: INIT, 45 | snapshot, 46 | }; 47 | } 48 | 49 | export function Reset(): Actions.Reset { 50 | return { 51 | type: RESET, 52 | }; 53 | } 54 | 55 | // ************************************************************************ // 56 | 57 | // ************************** // 58 | // *** ACTIONS INTERFACES *** // 59 | // ************************** // 60 | 61 | // ************************************************************************ // 62 | 63 | export declare namespace Actions { 64 | interface Init extends Action { 65 | snapshot: State; 66 | } 67 | 68 | interface Reset extends Action {} 69 | } 70 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { SelectableComponent } from "../../sections/useRegistrations"; 4 | import { createClassName } from "../../../../utils"; 5 | import useFallback from "../useFallback"; 6 | import useClickEvent from "../useClickEvent"; 7 | import { StylingProps } from "../../../../model"; 8 | import { PassField } from "../../../constants"; 9 | 10 | export { default as FieldLabel } from "./FieldLabel"; 11 | export { default as FieldValue } from "./FieldValue"; 12 | 13 | type Props = StylingProps & 14 | Partial & { 15 | fieldData: PassField; 16 | }; 17 | 18 | export function Field(props: React.PropsWithChildren) { 19 | /** 20 | * We don't want to pass the click event to children. 21 | * They will still accept it but only if used separately. 22 | */ 23 | const { 24 | onClick, 25 | className: sourceClassName, 26 | fieldData: { key, label, value }, 27 | style = {}, 28 | children, 29 | } = props; 30 | 31 | return useClickEvent( 32 | onClick, 33 | useFallback(() => { 34 | const className = createClassName(["field", sourceClassName], { 35 | [`field-${key ?? ""}`]: key, 36 | }); 37 | 38 | return ( 39 |
40 | {children} 41 |
42 | ); 43 | }, [label, value, key]) 44 | ); 45 | } 46 | 47 | /** 48 | * This components is needed to fallback in case of 49 | * self-handled FieldLabel and FieldValue. 50 | * 51 | * Used in Travel Primary Fields to make elements 52 | * fit in the grid. 53 | */ 54 | 55 | export function GhostField(props: React.PropsWithChildren) { 56 | const { 57 | onClick, 58 | fieldData: { key, label, value }, 59 | children, 60 | } = props; 61 | 62 | return useClickEvent( 63 | onClick, 64 | useFallback(() => { 65 | return <>{children}; 66 | }, [label, value, key]) 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/Configurator/Switcher/style.less: -------------------------------------------------------------------------------- 1 | /** Grasp the reference :) */ 2 | 3 | .the-switcher { 4 | display: flex; 5 | align-items: center; 6 | margin: 0 0.5rem; 7 | cursor: pointer; 8 | -webkit-tap-highlight-color: transparent; 9 | 10 | & i { 11 | position: relative; 12 | display: inline-block; 13 | margin-right: 0.5rem; 14 | width: 46px; 15 | height: 26px; 16 | background-color: #444444; 17 | border-radius: 23px; 18 | vertical-align: text-bottom; 19 | transition: all 0.3s linear; 20 | 21 | &::before { 22 | content: ""; 23 | position: absolute; 24 | left: 0; 25 | width: 42px; 26 | height: 22px; 27 | border-radius: 11px; 28 | transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1); 29 | transition: all 0.25s linear; 30 | } 31 | 32 | &::after { 33 | content: ""; 34 | position: absolute; 35 | left: 0; 36 | width: 22px; 37 | height: 22px; 38 | background-color: #fff; 39 | border-radius: 11px; 40 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24); 41 | transform: translate3d(2px, 2px, 0); 42 | transition: all 0.2s ease-in-out; 43 | } 44 | } 45 | 46 | & input { 47 | &:checked { 48 | & + i { 49 | background-color: #4bd763; 50 | 51 | &::before { 52 | transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0); 53 | } 54 | 55 | &::after { 56 | transform: translate3d(22px, 2px, 0); 57 | } 58 | } 59 | } 60 | 61 | &[disabled] { 62 | & + i { 63 | background-color: grey; 64 | } 65 | } 66 | } 67 | 68 | &:active { 69 | /** 70 | * Enable this instruction to apply an enlargment of 71 | * the toggle on animation (iOS 10, if I remember correctly). 72 | * Also decrease tx value below 73 | */ 74 | 75 | /*& i::after { 76 | width: 28px; 77 | transform: translate3d(2px, 2px, 0); 78 | }*/ 79 | 80 | & input:checked { 81 | & + i::after { 82 | // Decreasing value tx (22px) applies a traction effect 83 | transform: translate3d(22px, 2px, 0); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passkit-visual-designer", 3 | "version": "1.0.0-beta.9", 4 | "private": true, 5 | "description": "A visual tool to design passes", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "NODE_ENV=prod webpack --config webpack.config.js", 9 | "start": "npx webpack serve --config webpack.config.js --node-env dev" 10 | }, 11 | "keywords": [ 12 | "apple", 13 | "passkit" 14 | ], 15 | "author": "Alexander P. Cerutti ", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@babel/core": "^7.13.10", 19 | "@babel/preset-env": "^7.13.10", 20 | "@types/prismjs": "^1.16.3", 21 | "@types/react": "^17.0.3", 22 | "@types/react-color": "^2.17.5", 23 | "@types/react-dom": "^17.0.2", 24 | "@types/react-redux": "^7.1.16", 25 | "@types/react-router-dom": "^5.1.7", 26 | "@types/react-transition-group": "^4.4.1", 27 | "@types/uuid": "^8.3.0", 28 | "babel-loader": "^8.2.2", 29 | "css-loader": "^5.1.3", 30 | "file-loader": "^6.2.0", 31 | "fork-ts-checker-webpack-plugin": "^6.2.0", 32 | "handlebars-loader": "^1.7.1", 33 | "html-webpack-plugin": "^5.3.1", 34 | "less": "^4.1.1", 35 | "less-loader": "^8.0.0", 36 | "prettier": "^2.2.1", 37 | "style-loader": "^2.0.0", 38 | "thread-loader": "^3.0.1", 39 | "ts-loader": "^8.0.18", 40 | "typescript": "^4.2.3", 41 | "webpack": "^5.27.1", 42 | "webpack-cli": "^4.5.0", 43 | "webpack-dev-server": "^3.11.2" 44 | }, 45 | "dependencies": { 46 | "date-fns": "^2.19.0", 47 | "handlebars": "^4.7.7", 48 | "html2canvas": "^1.0.0-rc.7", 49 | "jszip": "^3.6.0", 50 | "localforage": "^1.9.0", 51 | "prismjs": "^1.23.0", 52 | "react": "^17.0.2", 53 | "react-color": "^2.19.3", 54 | "react-dom": "^17.0.2", 55 | "react-redux": "^7.2.3", 56 | "react-router-dom": "^5.2.0", 57 | "react-transition-group": "^4.4.1", 58 | "redux": "^4.0.5", 59 | "redux-devtools-extension": "^2.13.9", 60 | "redux-thunk": "^2.3.0", 61 | "tslib": "^2.1.0", 62 | "uuid": "^8.3.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/App/common.less: -------------------------------------------------------------------------------- 1 | .scrollableWithoutScrollbars(@direction) { 2 | & when(@direction = "both") { 3 | overflow: scroll; 4 | } 5 | 6 | & when(@direction = y), (@direction = x) { 7 | overflow-@{direction}: scroll; 8 | } 9 | 10 | scrollbar-width: none; /* Firefox only */ 11 | 12 | &::-webkit-scrollbar { 13 | display: none; 14 | } 15 | } 16 | 17 | .withImagesTrasparencyBackground(@setPositionRelative: false) { 18 | @chess-color: #ffffff20; 19 | 20 | & when (@setPositionRelative = true) { 21 | position: relative; 22 | } 23 | 24 | /** 25 | * Transparent-background images filler 26 | */ 27 | &::before { 28 | content: ""; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | right: 0; 33 | bottom: 0; 34 | z-index: 0; 35 | background-image: linear-gradient(45deg, @chess-color 25%, transparent 25%), 36 | linear-gradient(-45deg, @chess-color 25%, transparent 25%), 37 | linear-gradient(45deg, transparent 75%, @chess-color 75%), 38 | linear-gradient(-45deg, transparent 75%, @chess-color 75%); 39 | background-size: 20px 20px; 40 | background-position: 0 0, 0 10px, 10px -10px, -10px 0px; 41 | border: 1px solid @chess-color; 42 | } 43 | 44 | & > img { 45 | position: relative; 46 | z-index: 1 !important; 47 | } 48 | } 49 | 50 | .lookMumIamAppearing(@inTime, @tmf: ease-in-out, @delay: 0s) { 51 | opacity: 0; 52 | animation-name: appearance; 53 | animation-duration: @inTime; 54 | animation-timing-function: @tmf; 55 | animation-delay: @delay; 56 | animation-iteration-count: 1; 57 | animation-fill-mode: forwards; 58 | 59 | @keyframes appearance { 60 | to { 61 | opacity: 1; 62 | } 63 | } 64 | } 65 | 66 | .pagetransition() { 67 | /** Order is important **/ 68 | 69 | &.enter-active, 70 | &.exit-active { 71 | transition: opacity 0.5s; 72 | } 73 | 74 | &.enter { 75 | opacity: 0; 76 | } 77 | 78 | &.enter-active { 79 | opacity: 1; 80 | } 81 | 82 | &.exit { 83 | opacity: 1; 84 | } 85 | 86 | &.exit-active { 87 | opacity: 0; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PassSelector/PassList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { PassKind } from "../model"; 4 | import { PassMixedProps } from "@pkvd/pass"; 5 | import { createClassName } from "../utils"; 6 | import { SelectablePassProps } from "./SelectablePass"; 7 | 8 | interface PassListProps { 9 | onPassSelect(passProps: PassMixedProps): void; 10 | requiresAttention?: boolean; 11 | selectedKind?: PassKind; 12 | } 13 | 14 | type PassListPropsWithChildren = React.PropsWithChildren; 15 | 16 | export default function PassList(props: PassListPropsWithChildren): JSX.Element { 17 | const selectionTray = React.useRef(null); 18 | 19 | const onPassClickHandler = React.useCallback( 20 | (event: React.MouseEvent, clickProps: PassMixedProps) => { 21 | event.stopPropagation(); 22 | props.onPassSelect({ ...clickProps }); 23 | }, 24 | [] 25 | ); 26 | 27 | React.useEffect( 28 | () => 29 | void ( 30 | props.requiresAttention && 31 | selectionTray.current?.scrollIntoView({ behavior: "smooth", block: "end" }) 32 | ) 33 | ); 34 | 35 | const children = React.Children.map( 36 | props.children, 37 | (node: React.ReactElement) => { 38 | const { kind, name, ...passProps } = node.props; 39 | const className = createClassName(["select"], { 40 | highlighted: kind === props.selectedKind, 41 | }); 42 | 43 | return ( 44 |
onPassClickHandler(e, { kind, ...passProps })} 48 | > 49 | {node} 50 |
51 | ); 52 | } 53 | ); 54 | 55 | const className = createClassName([], { 56 | "space-first": children.length > 2, 57 | "element-first": children.length <= 2, 58 | "selection-active": props.selectedKind, 59 | }); 60 | 61 | return ( 62 |
63 |
64 | {children} 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/PanelsPage/Panel/ColorPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { TwitterPicker, RGBColor, ColorState } from "react-color"; 3 | import { SharedPanelProps } from ".."; 4 | import useContentSavingHandler from "../useContentSavingHandler"; 5 | import CapitalHeaderTitle from "../../../components/CapitalHeaderTitle"; 6 | import "./style.less"; 7 | import { FieldKind } from "../../../../../../model"; 8 | 9 | interface ColorPanelProps extends SharedPanelProps { 10 | value?: string; 11 | } 12 | 13 | export default function ColorPanel(props: ColorPanelProps) { 14 | const [color, onContentChangeHandler] = useContentSavingHandler( 15 | props.onValueChange, 16 | props.name, 17 | props.value || "rgb(0,0,0)" 18 | ); 19 | 20 | // Default react-color hashes 21 | const { current: colorHistory } = React.useRef([ 22 | "#ff6900", 23 | "#fcb900", 24 | "#7bdcb5", 25 | "#00d084", 26 | "#8ed1fc", 27 | "#0693e3", 28 | "#abb8c3", 29 | "#eb144c", 30 | "#f78da7", 31 | "#9900ef", 32 | ]); 33 | 34 | const onColorChange = React.useRef(({ rgb, hex }: ColorState) => { 35 | const colorInRecentlyUsedIndex = colorHistory.indexOf(hex); 36 | 37 | if (colorInRecentlyUsedIndex === -1) { 38 | colorHistory.unshift(hex); 39 | colorHistory.pop(); // to keep them 10 40 | } else if (colorInRecentlyUsedIndex !== 0) { 41 | colorHistory.unshift(...colorHistory.splice(colorInRecentlyUsedIndex, 1)); 42 | } 43 | 44 | const rgbColor = rgbaToRGBString(rgb); 45 | onContentChangeHandler(rgbColor); 46 | }); 47 | 48 | return ( 49 |
50 | 51 | 58 |
59 | ); 60 | } 61 | 62 | function rgbaToRGBString({ r, g, b }: RGBColor) { 63 | return `rgb(${r},${g},${b})`; 64 | } 65 | -------------------------------------------------------------------------------- /src/Configurator/MediaModal/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../App/common.less"; 2 | @import (reference) "../ModalBase/common.less"; 3 | 4 | @grid-gap: 10px; 5 | 6 | .modal { 7 | & > .modal-content#media-collection { 8 | top: 18%; 9 | left: 8%; 10 | right: 8%; 11 | bottom: 18%; 12 | 13 | & > * { 14 | background-color: @modal-background-color; 15 | display: flex; 16 | box-shadow: 0 0 5px 0px #3c3c3c; 17 | } 18 | 19 | #pass-preview { 20 | border-top-left-radius: 3px; 21 | border-bottom-left-radius: 3px; 22 | margin-right: 15px; 23 | flex-grow: 0; 24 | flex-shrink: 0; 25 | align-items: center; 26 | justify-content: center; 27 | width: 230px; /* fixed width */ 28 | padding: 0 30px; 29 | } 30 | 31 | #media-collector { 32 | flex-basis: 0; // to allow grid resizing through margins 33 | flex-grow: 2; 34 | border-top-right-radius: 3px; 35 | border-bottom-right-radius: 3px; 36 | flex-direction: column; 37 | position: relative; 38 | /** To allow ellipsis on text */ 39 | min-width: 0; 40 | 41 | & > header { 42 | min-height: 60px; 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | padding: 0 20px; 47 | 48 | & button.edit-mode { 49 | background: none; 50 | border: 0; 51 | outline: none; 52 | 53 | color: #e6e6e6; 54 | cursor: pointer; 55 | min-width: 40px; 56 | text-align: right; 57 | font-size: inherit; 58 | 59 | &[disabled] { 60 | color: #646464; 61 | cursor: initial; 62 | } 63 | } 64 | } 65 | 66 | & > .list { 67 | display: flex; 68 | flex-direction: column; 69 | flex-grow: 1; 70 | overflow: hidden; 71 | 72 | & > #grid { 73 | display: grid; 74 | column-gap: @grid-gap; 75 | row-gap: @grid-gap; 76 | padding: 20px; 77 | justify-items: center; 78 | align-items: start; 79 | grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); 80 | 81 | .scrollableWithoutScrollbars(y); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/BackFields/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../App/common.less"; 2 | 3 | .back-fields { 4 | .scrollableWithoutScrollbars(y); 5 | position: absolute; 6 | top: 0; 7 | bottom: 0; 8 | right: 0; 9 | left: 0; 10 | background-color: #010101; /* Like Apple Wallet iOS 13 in dark mode */ 11 | padding: 10px; 12 | border-radius: inherit; 13 | 14 | /* For backflip */ 15 | transform: rotateY(180deg); 16 | -webkit-backface-visibility: hidden; 17 | backface-visibility: hidden; 18 | 19 | display: flex; 20 | align-items: flex-start; 21 | 22 | & > .fields-row { 23 | flex-direction: column; 24 | width: 100%; 25 | min-height: initial; 26 | max-height: initial; 27 | margin-top: initial; 28 | justify-content: flex-start; 29 | height: 100%; 30 | 31 | /** 32 | * If empty placeholder is shown we want to 33 | * make the backfield to occupy the whole height 34 | */ 35 | & > .empty-field:only-child { 36 | background-color: #1c1c1e; 37 | 38 | &::before { 39 | content: "Set BackFields to make something to appear here like magic. ✨"; 40 | display: flex; 41 | align-items: center; 42 | color: #9c9ba0; 43 | text-align: center; 44 | line-height: 25px; 45 | font-size: 14px; 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | left: 0; 50 | bottom: 0; 51 | padding: 20px; 52 | pointer-events: none; 53 | } 54 | } 55 | 56 | & .field { 57 | padding: 7px 10px; 58 | background-color: #1c1c1e; 59 | 60 | &:first-child { 61 | border-radius: 7px 7px 0 0; 62 | } 63 | 64 | &:last-child { 65 | border-radius: 0 0 7px 7px; 66 | } 67 | 68 | &:not(:last-child) { 69 | border-bottom: 1px solid #323234; 70 | } 71 | 72 | & .label { 73 | min-height: 16px; 74 | color: #9c9ba0; 75 | text-transform: initial; 76 | padding-right: 7px; 77 | font-size: 8pt; 78 | } 79 | 80 | & .value { 81 | min-height: 16px; 82 | color: #9c9ba0; 83 | padding-right: 7px; 84 | white-space: pre-wrap; /* New lines, here I come! */ 85 | font-size: 9pt; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Pass/layouts/sections/Header/style.less: -------------------------------------------------------------------------------- 1 | @fieldsSpacing: 5px; 2 | 3 | .header-container { 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | min-height: 20px; 8 | width: 100%; 9 | flex-wrap: wrap; 10 | overflow: hidden; 11 | margin-bottom: 25px; 12 | max-height: 35px; 13 | 14 | /** 15 | * The desired behavior would be to make .inner 16 | * to nest elements (like .fields-row) after 17 | * making its element wrapping on itself, 18 | * but it seems there is not a way to specify the order... 19 | * So, for now we won't make exceeding items wrap 20 | * in .inner but just overflow 21 | */ 22 | 23 | & .inner { 24 | display: inline-flex; 25 | flex: auto; 26 | align-items: center; 27 | 28 | & > *:not(:last-child) { 29 | margin-right: @fieldsSpacing; 30 | } 31 | 32 | & > .image-field { 33 | display: flex; 34 | align-self: flex-end; 35 | 36 | & > img { 37 | max-height: 15px; 38 | } 39 | } 40 | 41 | & > .empty-field { 42 | height: 22px; 43 | 44 | &:first-child { 45 | border-top-left-radius: 2px; 46 | border-bottom-left-radius: 2px; 47 | 48 | // to give the element a static size 49 | flex-grow: 0; 50 | flex-basis: 50px; 51 | } 52 | 53 | &:nth-child(2) { 54 | flex-grow: 2; 55 | } 56 | } 57 | 58 | /** 59 | * Known issue: when not all fields can fit, they 60 | * will, on purpose, get wrapped but fields row will 61 | * still try to take space they deserve (maybe it can 62 | * solved with flex-basis: content, but it is pretty 63 | * unsupported yet). 64 | */ 65 | 66 | & .fields-row { 67 | margin-top: 0; 68 | justify-content: flex-end; 69 | width: auto; 70 | flex-shrink: 1; 71 | 72 | &.can-grow { 73 | flex-grow: 1; 74 | } 75 | 76 | & .field:not(:last-child) { 77 | margin-right: @fieldsSpacing; 78 | } 79 | 80 | & .empty-field { 81 | border-radius: 0; 82 | border-top-right-radius: 2px; 83 | border-bottom-right-radius: 2px; 84 | 85 | // to give the element a static size 86 | flex-grow: 0; 87 | flex-basis: 50px; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/navigation.utils/navigation.memory.tsx: -------------------------------------------------------------------------------- 1 | import { PassMixedProps } from "@pkvd/pass"; 2 | 3 | let pages: Page = null; 4 | const updateListeners: Function[] = []; 5 | 6 | interface Page { 7 | uuid: string; 8 | // Added automatically 9 | next: Page | null; 10 | index?: number; // added automatically 11 | } 12 | 13 | export function getPagesAmount(): number { 14 | if (!pages) { 15 | return 0; 16 | } 17 | 18 | let lastElement = pages; 19 | let count = 0; 20 | 21 | while (lastElement.next !== null) { 22 | count++; 23 | lastElement = lastElement.next; 24 | } 25 | 26 | return count; 27 | } 28 | 29 | export function registerListener(listener: (amount: number) => void) { 30 | updateListeners.push(listener); 31 | } 32 | 33 | export function sendUpdates() { 34 | const pagesAmount = getPagesAmount(); 35 | 36 | for (let listener of updateListeners) { 37 | listener(pagesAmount); 38 | } 39 | } 40 | 41 | export function getLastPage() { 42 | let lastElement = pages; 43 | 44 | if (!lastElement) { 45 | return null; 46 | } 47 | 48 | while (lastElement.next !== null) { 49 | lastElement = lastElement.next; 50 | } 51 | 52 | return lastElement; 53 | } 54 | 55 | export function addPage(uuid: string) { 56 | let lastElement = pages; 57 | 58 | if (!lastElement) { 59 | pages = { 60 | uuid, 61 | next: null, 62 | index: 0, 63 | }; 64 | 65 | return pages; 66 | } 67 | 68 | lastElement = getLastPage(); 69 | lastElement.next = { 70 | uuid, 71 | next: null, 72 | index: lastElement.index + 1, 73 | }; 74 | 75 | return lastElement.next; 76 | } 77 | 78 | export function removePageChain(uuid: string) { 79 | let lastElement = pages; 80 | 81 | if (!lastElement) { 82 | return; 83 | } 84 | 85 | while (lastElement.next !== null) { 86 | if (lastElement.next && lastElement.next.uuid === uuid) { 87 | break; 88 | } 89 | 90 | lastElement = lastElement.next; 91 | } 92 | 93 | lastElement.next = null; 94 | return; 95 | } 96 | 97 | export function reset() { 98 | pages = null; 99 | } 100 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/Drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { MoreFieldsBelowIcon } from "../icons"; 5 | import DrawerElement from "../DrawerElement"; 6 | 7 | type PassField = Constants.PassField; 8 | 9 | interface Props { 10 | readonly fieldsData: PassField[]; 11 | onFieldDelete(fieldUUID: string): void; 12 | onFieldOrderChange(fieldUUID: string, of: number): void; 13 | openDetailsPage(fieldUUID: string): void; 14 | } 15 | 16 | export default function Drawer(props: Props) { 17 | const [isThereMoreAfterTheSkyline, setMoreAvailability] = React.useState(false); 18 | const drawerRef = React.useRef(); 19 | 20 | const onListScrollHandler = React.useCallback( 21 | ({ currentTarget }: Partial>) => { 22 | // Tollerance of 25px before showing the indicator 23 | const didReachEndOfScroll = 24 | currentTarget.scrollHeight - currentTarget.scrollTop <= currentTarget.clientHeight + 25; 25 | // We want to hide "more" icon if we reached end of the scroll 26 | setMoreAvailability(!didReachEndOfScroll); 27 | }, 28 | [] 29 | ); 30 | 31 | React.useEffect(() => { 32 | const { current: currentTarget } = drawerRef; 33 | 34 | if (props.fieldsData.length) { 35 | onListScrollHandler({ currentTarget }); 36 | } 37 | }, [props.fieldsData]); 38 | 39 | const panels = props.fieldsData.map((field, index) => { 40 | return ( 41 | 50 | ); 51 | }); 52 | 53 | return ( 54 | <> 55 |
56 | {panels} 57 |
58 |
59 | 60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Barcodes/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PKBarcodeFormat, WalletPassFormat } from "../../../constants"; 3 | import QRCode from "./qr-code"; 4 | import Code128 from "./code128"; 5 | import PDF417 from "./pdf417"; 6 | import Aztec from "./aztec"; 7 | import { EmptyBarcode, EmptySquareCode } from "./empty"; 8 | import "./style.less"; 9 | import { createClassName } from "../../../../utils"; 10 | 11 | interface BarcodeProps extends Partial { 12 | fallbackShape: "square" | "rect"; 13 | 14 | // @TODO 15 | voided?: boolean; 16 | } 17 | 18 | export default function Barcodes(props: BarcodeProps) { 19 | const barcodeFormat = props.format || PKBarcodeFormat.None; 20 | const BarcodeComponent = selectComponentFromFormat(barcodeFormat, props.fallbackShape); 21 | 22 | if (!BarcodeComponent) { 23 | return null; 24 | } 25 | 26 | const className = createClassName(["barcode", barcodeFormat, props.fallbackShape], { 27 | content: barcodeFormat !== PKBarcodeFormat.None && props.message, 28 | }); 29 | 30 | return ( 31 |
32 |
33 | 34 | {(props.format && props.message) ?? null} 35 |
36 |
37 | ); 38 | } 39 | 40 | export function isSquareBarcode(kind: PKBarcodeFormat) { 41 | return ( 42 | kind === PKBarcodeFormat.Square || kind === PKBarcodeFormat.QR || kind === PKBarcodeFormat.Aztec 43 | ); 44 | } 45 | 46 | export function isRectangularBarcode(kind: PKBarcodeFormat) { 47 | return ( 48 | kind === PKBarcodeFormat.Rectangle || 49 | kind === PKBarcodeFormat.Code128 || 50 | kind === PKBarcodeFormat.PDF417 51 | ); 52 | } 53 | 54 | function selectComponentFromFormat(format: PKBarcodeFormat, fallbackFormat: "square" | "rect") { 55 | switch (format) { 56 | case PKBarcodeFormat.Aztec: 57 | return Aztec; 58 | case PKBarcodeFormat.Code128: 59 | return Code128; 60 | case PKBarcodeFormat.PDF417: 61 | return PDF417; 62 | case PKBarcodeFormat.QR: 63 | return QRCode; 64 | default: 65 | if (fallbackFormat === "square") { 66 | return EmptySquareCode; 67 | } 68 | 69 | return EmptyBarcode; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Pass/layouts/EventTicket.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PassMixedProps } from ".."; 3 | import { PassHeader } from "./sections/Header"; 4 | import { StripPrimaryFields, ThumbnailPrimaryFields } from "./sections/PrimaryFields"; 5 | import FieldsRow from "./sections/FieldRow"; 6 | import Footer from "./sections/Footer"; 7 | import Barcodes from "./components/Barcodes"; 8 | import { useRegistrations } from "./sections/useRegistrations"; 9 | import { FieldKind } from "../../model"; 10 | 11 | type PrimaryFieldPropsKind = Parameters< 12 | typeof StripPrimaryFields | typeof ThumbnailPrimaryFields 13 | >[0]; 14 | 15 | export default function EventTicket(props: PassMixedProps): JSX.Element { 16 | const { 17 | secondaryFields = [], 18 | primaryFields = [], 19 | headerFields = [], 20 | auxiliaryFields = [], 21 | barcode, 22 | logoText, 23 | logo, 24 | icon, 25 | stripImage, 26 | thumbnailImage, 27 | } = props; 28 | 29 | let fieldsFragment: React.ReactElement; 30 | 31 | const SecondaryFieldRow = ( 32 | 33 | ); 34 | 35 | /** We fallback to strip image model if none of the required property is available */ 36 | if (props.hasOwnProperty("stripImage") || !props.hasOwnProperty("backgroundImage")) { 37 | fieldsFragment = ( 38 | <> 39 | 40 | {SecondaryFieldRow} 41 | 42 | ); 43 | } else if (props.hasOwnProperty("backgroundImage")) { 44 | useRegistrations([[FieldKind.IMAGE, "backgroundImage"]]); 45 | 46 | fieldsFragment = ( 47 | 48 | {SecondaryFieldRow} 49 | 50 | ); 51 | } 52 | 53 | return ( 54 | <> 55 | 56 | {fieldsFragment} 57 | 58 |
59 | 60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPreviewPage/DrawerElement/FieldOptionsBar/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function ListAddProp(props: React.SVGProps) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | ); 17 | } 18 | 19 | export function DeleteFieldIcon(props: React.SVGProps) { 20 | return ( 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/projectOptions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | import { initialState, State } from "."; 3 | 4 | export type POKeys = keyof State["projectOptions"]; 5 | export type POValues = State["projectOptions"][POKeys]; 6 | 7 | // ************************************************************************ // 8 | 9 | // ********************* // 10 | // *** ACTION TYPES *** // 11 | // ********************* // 12 | 13 | // ************************************************************************ // 14 | 15 | export const SET_OPTION = "options/SET_OPTION"; 16 | 17 | // ************************************************************************ // 18 | 19 | // ********************* // 20 | // *** MEDIA REDUCER *** // 21 | // ********************* // 22 | 23 | // ************************************************************************ // 24 | 25 | export default function reducer( 26 | state = initialState.projectOptions, 27 | action: Actions.Set 28 | ): State["projectOptions"] { 29 | switch (action.type) { 30 | case SET_OPTION: { 31 | if (!action.value) { 32 | const stateCopy = { ...state }; 33 | 34 | delete stateCopy[action.key]; 35 | return stateCopy; 36 | } 37 | 38 | return { 39 | ...state, 40 | [action.key]: action.value, 41 | }; 42 | } 43 | 44 | default: { 45 | return state; 46 | } 47 | } 48 | } 49 | 50 | // ************************************************************************ // 51 | 52 | // *********************** // 53 | // *** ACTION CREATORS *** // 54 | // *********************** // 55 | 56 | // ************************************************************************ // 57 | 58 | export function Set(key: POKeys, value: POValues): Actions.Set { 59 | return { 60 | type: SET_OPTION, 61 | key, 62 | value, 63 | }; 64 | } 65 | 66 | // ************************************************************************ // 67 | 68 | // ************************** // 69 | // *** ACTIONS INTERFACES *** // 70 | // ************************** // 71 | 72 | // ************************************************************************ // 73 | 74 | export declare namespace Actions { 75 | interface Set extends Action { 76 | key: POKeys; 77 | value: POValues; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Configurator/MediaModal/icons.tsx: -------------------------------------------------------------------------------- 1 | // Plus by Kirk Draheim from the Noun Project 2 | // https://thenounproject.com/term/plus/3539744 3 | 4 | import * as React from "react"; 5 | 6 | export function PlusIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | // edit by Markus from the Noun Project (edited) 15 | // https://thenounproject.com/term/edit/3126120 16 | 17 | export function EditIcon(props: React.SVGProps) { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | // @TODO Link to the one in pages - this has been brought here from there 26 | // Arrow by Kirsh from the Noun Project (edited) 27 | // https://thenounproject.com/term/arrow/1256496 28 | 29 | export function ArrowIcon(props: React.SVGProps) { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | // Delete by Fantastic from the Noun Project (edited) 38 | // https://thenounproject.com/term/delete/1393049 39 | 40 | export function DeleteIcon(props: React.SVGProps) { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/FieldPropertiesEditList/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { FieldPropertiesDetails } from "../../FieldsPreviewPage/DrawerElement/FieldPropertiesDetails"; 5 | import FieldStringPropertyPanel from "./FieldPropertyPanels/String"; 6 | import FieldCheckboxPropertyPanel from "./FieldPropertyPanels/Checkbox"; 7 | import FieldEnumPropertyPanel from "./FieldPropertyPanels/Enum"; 8 | 9 | const { PKTextAlignment, PKDateStyle, PKDataDetectorType } = Constants; 10 | 11 | type PassField = Constants.PassField; 12 | 13 | interface FieldPropertiesEditListProps { 14 | data: PassField; 15 | onValueChange(prop: string, value: T): void; 16 | } 17 | 18 | export default function FieldPropertiesEditList(props: FieldPropertiesEditListProps) { 19 | const properties = FieldPropertiesDetails.map(({ name, type, placeholder, defaultValue }) => { 20 | const valueFromData = props.data[name]; 21 | 22 | if (isPanelTypeEnum(type)) { 23 | return ( 24 | 32 | ); 33 | } 34 | 35 | if (isPanelTypeString(type)) { 36 | return ( 37 | 44 | ); 45 | } 46 | 47 | if (isPanelTypeCheckbox(type)) { 48 | return ( 49 | 55 | ); 56 | } 57 | }); 58 | 59 | return
{properties}
; 60 | } 61 | 62 | function isPanelTypeEnum(type: Object) { 63 | return type === PKTextAlignment || type === PKDateStyle || type === PKDataDetectorType; 64 | } 65 | 66 | function isPanelTypeString(type: Object) { 67 | return type === String; 68 | } 69 | 70 | function isPanelTypeCheckbox(type: Object) { 71 | return type === Boolean; 72 | } 73 | -------------------------------------------------------------------------------- /src/RecentSelector/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Official Github Logo 5 | * @see https://github.com/logos 6 | */ 7 | 8 | export function GithubLogoDarkMode(props: React.SVGProps) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 19 | 23 | 24 | ); 25 | } 26 | 27 | // add by Harper from the Noun Project 28 | // https://thenounproject.com/term/add/1623623 29 | 30 | export function AddIcon(props: React.SVGProps) { 31 | return ( 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/Configurator/LanguageModal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import Modal, { ModalProps } from "../ModalBase"; 4 | import { languages } from "./languages"; 5 | import { createClassName } from "../../utils"; 6 | 7 | interface Props extends Omit { 8 | currentLanguage: string; 9 | usedLanguages: Set; 10 | selectLanguage(languageCode: string): void; 11 | } 12 | 13 | export default function LanguageModal(props: Props) { 14 | const languagesRegions = Object.entries(languages).map(([region, languages]) => { 15 | const languagesList = Object.entries(languages).map(([ISO639alpha1, language]) => { 16 | const className = createClassName([], { 17 | used: props.usedLanguages.has(ISO639alpha1), 18 | current: ISO639alpha1 === props.currentLanguage, 19 | }); 20 | 21 | return ( 22 |
props.selectLanguage(ISO639alpha1)} 26 | title={`Language code: ${ISO639alpha1}`} 27 | > 28 | {language} 29 |
30 | ); 31 | }); 32 | 33 | return ( 34 | 35 |

{region}

36 |
{languagesList}
37 |
38 | ); 39 | }); 40 | 41 | const defaultLanguageClassName = createClassName([], { 42 | used: props.usedLanguages.has("default"), 43 | current: props.currentLanguage === "default", 44 | }); 45 | 46 | return ( 47 | 48 |
49 |

Choo-choose a language

50 |

51 | The list is contains languages supported by Apple. Keep cursor on a language to see its 52 | ISO 639-1 code. 53 |

54 |
55 |
56 |

Default

57 |
58 |
props.selectLanguage("default")} 61 | title={ 62 | "This is the root folder of your pass. Any media / translation here will be valid for each unused or not overriden language" 63 | } 64 | > 65 | Default (root) 66 |
67 |
68 | {languagesRegions} 69 |
70 |
71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/components/FieldPreview/style.less: -------------------------------------------------------------------------------- 1 | @base-background-color: #1c1c1c; 2 | 3 | .field-preview { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | padding: 0 5px; 9 | 10 | &:not(.editable) { 11 | cursor: pointer; 12 | } 13 | 14 | & .preview-field-key { 15 | width: 100%; 16 | border-bottom: 1px solid #272727; 17 | font-weight: 100; 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: baseline; 21 | padding: 18px 10px; 22 | box-sizing: border-box; 23 | 24 | &.none { 25 | & span { 26 | color: #6f6f6f; 27 | } 28 | } 29 | 30 | &::before { 31 | content: "Key:"; 32 | color: #e6e6e6; 33 | letter-spacing: 1px; 34 | margin-right: 15px; 35 | font-size: 13pt; 36 | } 37 | 38 | & input { 39 | background: transparent; 40 | border: 0; 41 | border-bottom: 1px solid #2f2f2f; 42 | font-size: 12pt; 43 | max-width: 150px; 44 | outline: none; 45 | color: #e6e6e6; 46 | } 47 | } 48 | 49 | &.hidden { 50 | & .preview-main-box { 51 | & span.label, 52 | & span.value { 53 | color: #757575; 54 | } 55 | 56 | & span.label { 57 | background-color: #2b2b2b; 58 | } 59 | 60 | & span.value { 61 | background-color: #21201e; 62 | } 63 | } 64 | } 65 | 66 | & .preview-main-box { 67 | display: flex; 68 | flex-direction: column; 69 | letter-spacing: 0.5px; 70 | text-transform: uppercase; 71 | margin: 20px auto 10px auto; 72 | max-width: 100%; 73 | 74 | &.align-right > * { 75 | text-align: right; 76 | } 77 | &.align-left > * { 78 | text-align: left; 79 | } 80 | &.align-center > * { 81 | text-align: center; 82 | } 83 | &.align-natural > * { 84 | text-align: start; 85 | } 86 | 87 | & span.label, 88 | & span.value { 89 | color: @base-background-color; 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | white-space: nowrap; 93 | flex-grow: 1; 94 | padding: 3px 6px; 95 | } 96 | 97 | & span.label { 98 | font-size: 11pt; 99 | background-color: #ffab00; 100 | border-radius: 2px 2px 0 0; 101 | } 102 | 103 | & span.value { 104 | font-size: 16pt; 105 | background-color: #ff830e; 106 | border-radius: 0 0 2px 2px; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/FieldsPropertiesEditPage/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants, PassMixedProps } from "@pkvd/pass"; 4 | import * as Store from "@pkvd/store"; 5 | import PageHeader from "../components/Header"; 6 | import FieldPreview from "../components/FieldPreview"; 7 | import FieldPropertiesEditList from "./FieldPropertiesEditList"; 8 | import { PageContainer } from "../../PageContainer"; 9 | import { PageProps } from "../../navigation.utils"; 10 | import { connect } from "react-redux"; 11 | 12 | type PassField = Constants.PassField; 13 | 14 | interface Props extends PageProps { 15 | selectedField?: PassField[]; 16 | fieldUUID: string; 17 | changePassPropValue?: typeof Store.Pass.setProp; 18 | } 19 | 20 | class FieldsPropertiesEditPage extends React.Component { 21 | private dataIndex: number; 22 | 23 | constructor(props: Props) { 24 | super(props); 25 | 26 | this.dataIndex = this.props.selectedField.findIndex( 27 | (field) => field.fieldUUID === this.props.fieldUUID 28 | ); 29 | 30 | this.updateValue = this.updateValue.bind(this); 31 | this.updatePassProp = this.updatePassProp.bind(this); 32 | this.updateKey = this.updateKey.bind(this); 33 | } 34 | 35 | updateValue(newData: PassField) { 36 | const allFieldsCopy = [...this.props.selectedField]; 37 | allFieldsCopy.splice(this.dataIndex, 1, newData); 38 | 39 | this.props.changePassPropValue(this.props.name as keyof PassMixedProps, allFieldsCopy); 40 | } 41 | 42 | updatePassProp(prop: string, value: T) { 43 | this.updateValue({ ...this.props.selectedField[this.dataIndex], [prop]: value }); 44 | } 45 | 46 | updateKey(value: string) { 47 | this.updatePassProp("key", value); 48 | } 49 | 50 | render() { 51 | const current = this.props.selectedField[this.dataIndex]; 52 | 53 | return ( 54 | 55 |
56 | 57 | 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | export default connect( 66 | (store: Store.State, ownProps: Props) => { 67 | const { pass } = store; 68 | 69 | const selectedField = pass[ownProps.name as keyof Constants.PassFields]; 70 | 71 | return { 72 | selectedField, 73 | }; 74 | }, 75 | { 76 | changePassPropValue: Store.Pass.setProp, 77 | } 78 | )(FieldsPropertiesEditPage); 79 | -------------------------------------------------------------------------------- /src/Configurator/OptionsMenu/pages/components/FieldPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import { Constants } from "@pkvd/pass"; 4 | import { createClassName } from "../../../../../utils"; 5 | import CommittableTextInput from "../../../../CommittableTextInput"; 6 | 7 | const { PKTextAlignment } = Constants; 8 | type PassField = Constants.PassField; 9 | 10 | interface Props { 11 | previewData: PassField; 12 | isFieldHidden?: boolean; 13 | keyEditable?: boolean; 14 | onClick?(): void; 15 | onFieldKeyChange?(newValue: string): void; 16 | } 17 | 18 | export default function FieldPreview(props: Props) { 19 | const [key, setKey] = React.useState(props.previewData?.key); 20 | 21 | const onFieldKeyChange = React.useCallback( 22 | (key: string) => { 23 | if (key !== props.previewData.key) { 24 | props.onFieldKeyChange(key); 25 | } 26 | }, 27 | [props.previewData.key] 28 | ); 29 | 30 | /** Effect to update the state value when props changes */ 31 | 32 | React.useEffect(() => { 33 | const { key: fieldKey } = props.previewData; 34 | if (fieldKey && fieldKey !== key) { 35 | setKey(fieldKey); 36 | } 37 | }, [props.previewData.key]); 38 | 39 | const FPClassName = createClassName(["field-preview"], { 40 | hidden: props.isFieldHidden, 41 | editable: props.keyEditable, 42 | }); 43 | 44 | const previewKeyClassName = createClassName(["preview-field-key"], { 45 | none: !key, 46 | }); 47 | 48 | const fieldKeyRow = props.keyEditable ? ( 49 | setKey(evt.target.value.replace(/\s+/, ""))} 51 | value={key || ""} 52 | placeholder="field's key" 53 | commit={onFieldKeyChange} 54 | /> 55 | ) : ( 56 | {!key ? "not setted" : key} 57 | ); 58 | 59 | const { textAlignment } = props.previewData ?? {}; 60 | 61 | const fieldStylesClassName = createClassName(["preview-main-box"], { 62 | "align-left": textAlignment === PKTextAlignment.Left, 63 | "align-right": textAlignment === PKTextAlignment.Right, 64 | "align-center": textAlignment === PKTextAlignment.Center, 65 | "align-natural": textAlignment === PKTextAlignment.Natural || !textAlignment, 66 | // @TODO: style dates 67 | }); 68 | 69 | return ( 70 |
71 |
{fieldKeyRow}
72 |
73 | {props.previewData?.label || "label"} 74 | {props.previewData?.value || "value"} 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/Configurator/staticFields.ts: -------------------------------------------------------------------------------- 1 | import { FieldDetails } from "./OptionsMenu/pages/PanelsPage/Panel"; 2 | import { FieldKind } from "../model"; 3 | import { DataGroup } from "./OptionsMenu/pages/PanelsPage"; 4 | 5 | const StaticFields: Array<[DataGroup, FieldDetails[]]> = [ 6 | [ 7 | DataGroup.METADATA, 8 | [ 9 | { 10 | name: "description", 11 | kind: FieldKind.TEXT, 12 | group: DataGroup.METADATA, 13 | mockable: false, 14 | tooltipText: "", 15 | disabled: false, 16 | required: true, 17 | }, 18 | /* { 19 | name: "formatVersion", 20 | kind: FieldKind.SWITCH, 21 | mockable: false, 22 | tooltipText: "", 23 | disabled: true, 24 | required: true 25 | },*/ { 26 | name: "organizationName", 27 | kind: FieldKind.TEXT, 28 | group: DataGroup.METADATA, 29 | required: true, 30 | }, 31 | { 32 | name: "passTypeIdentifier", 33 | kind: FieldKind.TEXT, 34 | group: DataGroup.METADATA, 35 | required: true, 36 | }, 37 | { 38 | name: "teamIdentifier", 39 | kind: FieldKind.TEXT, 40 | group: DataGroup.METADATA, 41 | required: true, 42 | }, 43 | { 44 | name: "appLaunchURL", 45 | kind: FieldKind.TEXT, 46 | group: DataGroup.METADATA, 47 | }, 48 | { 49 | name: "associatedStoreIdentifiers", 50 | kind: FieldKind.TEXT, 51 | group: DataGroup.METADATA, 52 | }, 53 | { 54 | name: "authenticationToken", 55 | kind: FieldKind.TEXT, 56 | group: DataGroup.METADATA, 57 | }, 58 | { 59 | name: "webServiceURL", 60 | kind: FieldKind.TEXT, 61 | group: DataGroup.METADATA, 62 | }, 63 | { 64 | name: "groupingIdentifier", 65 | kind: FieldKind.TEXT, 66 | group: DataGroup.METADATA, 67 | } /*, { 68 | name: "becons", 69 | kind: FieldKind.JSON, 70 | jsonKeys: ["major", "minor", "proximityUUID", "relevantText"] 71 | }, { 72 | name: "locations", 73 | kind: FieldKind.JSON, 74 | jsonKeys: ["altitude", "latitude", "longitude", "relevantText"] 75 | }, */, 76 | ], 77 | ], 78 | [ 79 | DataGroup.COLORS, 80 | [ 81 | { 82 | name: "backgroundColor", 83 | kind: FieldKind.COLOR, 84 | group: DataGroup.COLORS, 85 | }, 86 | { 87 | name: "foregroundColor", 88 | kind: FieldKind.COLOR, 89 | group: DataGroup.COLORS, 90 | }, 91 | { 92 | name: "labelColor", 93 | kind: FieldKind.COLOR, 94 | group: DataGroup.COLORS, 95 | }, 96 | ], 97 | ], 98 | [DataGroup.IMAGES, []], 99 | [DataGroup.DATA, []], 100 | ]; 101 | 102 | export default StaticFields; 103 | -------------------------------------------------------------------------------- /src/Configurator/Viewer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "./style.less"; 3 | import Pass, { PassProps, Constants, PassMixedProps } from "@pkvd/pass"; 4 | import { createClassName } from "../../utils"; 5 | import CommittableTextInput from "../CommittableTextInput"; 6 | import type { TranslationsSet } from "@pkvd/store"; 7 | 8 | type PassField = Constants.PassField; 9 | 10 | export interface ViewerProps extends Pick { 11 | passProps: PassMixedProps; 12 | translationSet: TranslationsSet; 13 | showEmpty: boolean; 14 | onVoidClick(e: React.MouseEvent): void; 15 | projectTitle?: string; 16 | changeProjectTitle(title: string): void; 17 | } 18 | 19 | export default function Viewer(props: ViewerProps) { 20 | const { changeProjectTitle, onVoidClick, projectTitle = "", showBack, passProps } = props; 21 | 22 | const viewerCN = createClassName(["viewer"], { 23 | "no-empty": !props.showEmpty, 24 | }); 25 | 26 | const passUIProps = { ...passProps }; 27 | 28 | if (props.translationSet?.enabled) { 29 | const translations = Object.values(props.translationSet.translations); 30 | 31 | Object.assign(passUIProps, { 32 | primaryFields: localizeFieldContent(passProps["primaryFields"], translations), 33 | secondaryFields: localizeFieldContent(passProps["secondaryFields"], translations), 34 | auxiliaryFields: localizeFieldContent(passProps["auxiliaryFields"], translations), 35 | backFields: localizeFieldContent(passProps["backFields"], translations), 36 | headerFields: localizeFieldContent(passProps["headerFields"], translations), 37 | }); 38 | } 39 | 40 | return ( 41 |
42 |
43 | 49 |
50 | 51 |
52 | ); 53 | } 54 | 55 | function localizeFieldContent( 56 | field: PassField[], 57 | translations: Array 58 | ) { 59 | if (!field) { 60 | return field; 61 | } 62 | 63 | return field.reduce((acc, field) => { 64 | const localizableElements = { label: field.label, value: field.value }; 65 | 66 | return [ 67 | ...acc, 68 | Object.assign( 69 | { ...field }, 70 | Object.entries(localizableElements).reduce((acc, [key, element]) => { 71 | if (!element) { 72 | return acc; 73 | } 74 | 75 | return { 76 | ...acc, 77 | [key]: translations.find(([placeholder]) => placeholder === element)?.[1] ?? element, 78 | }; 79 | }, {}) 80 | ), 81 | ]; 82 | }, []); 83 | } 84 | -------------------------------------------------------------------------------- /src/store/pass.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | import { ThunkAction } from "redux-thunk"; 3 | import { PassMixedProps } from "@pkvd/pass"; 4 | import { PassKind } from "../model"; 5 | import { initialState, State } from "."; 6 | 7 | // ************************************************************************ // 8 | 9 | // ********************* // 10 | // *** ACTION TYPES *** // 11 | // ********************* // 12 | 13 | // ************************************************************************ // 14 | 15 | export const SET_PROP = "pass/SET_PROP"; 16 | export const SET_PASS_KIND = "pass/SET_KIND"; 17 | export const SET_PROPS = "pass/SET_PROPS_BATCH"; 18 | 19 | // ************************************************************************ // 20 | 21 | // ********************* // 22 | // *** MEDIA REDUCER *** // 23 | // ********************* // 24 | 25 | // ************************************************************************ // 26 | 27 | export default function reducer(state = initialState.pass, action: Actions.SetProp): State["pass"] { 28 | switch (action.type) { 29 | case SET_PROP: { 30 | if (!action.value && state[action.key]) { 31 | const stateCopy = { ...state }; 32 | delete stateCopy[action.key]; 33 | 34 | return stateCopy; 35 | } 36 | 37 | return { 38 | ...state, 39 | [action.key]: action.value, 40 | }; 41 | } 42 | 43 | default: { 44 | return state; 45 | } 46 | } 47 | } 48 | 49 | // ************************************************************************ // 50 | 51 | // *********************** // 52 | // *** ACTION CREATORS *** // 53 | // *********************** // 54 | 55 | // ************************************************************************ // 56 | 57 | export function setProp( 58 | key: Actions.SetProp["key"], 59 | value: Actions.SetProp["value"] 60 | ): Actions.SetProp { 61 | return { 62 | type: SET_PROP, 63 | key, 64 | value, 65 | }; 66 | } 67 | 68 | export function setKind(kind: PassKind) { 69 | return setProp("kind", kind); 70 | } 71 | 72 | export function setPropsBatch(props: PassMixedProps): ThunkAction { 73 | return (dispatch) => { 74 | const keys = Object.keys(props) as (keyof PassMixedProps)[]; 75 | for (let i = keys.length, key: keyof PassMixedProps; (key = keys[--i]); ) { 76 | dispatch(setProp(key, props[key])); 77 | } 78 | }; 79 | } 80 | 81 | // ************************************************************************ // 82 | 83 | // ************************** // 84 | // *** ACTIONS INTERFACES *** // 85 | // ************************** // 86 | 87 | // ************************************************************************ // 88 | 89 | export declare namespace Actions { 90 | interface SetProp extends Action { 91 | key: keyof PassMixedProps; 92 | value: PassMixedProps[keyof PassMixedProps]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Configurator/ExportModal/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../ModalBase/common.less"; 2 | @import (reference) "../../App/common.less"; 3 | 4 | .modal { 5 | & > .modal-content#export { 6 | top: 10%; 7 | left: 10%; 8 | right: 10%; 9 | bottom: 10%; 10 | flex-direction: column; 11 | background-color: @modal-background-color; 12 | border-radius: 3px; 13 | padding: 30px; 14 | overflow: hidden; 15 | 16 | box-shadow: 0px 0px 10px 0px #131313; 17 | 18 | & h2 { 19 | margin: 10px 0 0; 20 | font-size: 3em; 21 | letter-spacing: 3px; 22 | transition: all 0.5s ease-in-out 1.5s; 23 | position: relative; 24 | 25 | &:hover { 26 | cursor: wait; 27 | 28 | &, 29 | & > span { 30 | letter-spacing: 20px; 31 | text-shadow: 0 1px 8px #000; 32 | } 33 | 34 | & span { 35 | transition: width 0.5s ease-in-out 3s, opacity 0.5s ease-in-out 3.2s; 36 | opacity: 1; 37 | width: 190px; 38 | } 39 | } 40 | 41 | &:not(:hover) { 42 | transition: all 0.5s ease-in-out 3s; 43 | 44 | & span { 45 | transition: opacity 0.5s ease-in-out 1s, width 0.5s ease-in-out 1.5s; 46 | } 47 | } 48 | 49 | & span { 50 | opacity: 0; 51 | width: 0px; 52 | display: inline-block; 53 | 54 | &:hover { 55 | animation-name: brue-rotate; 56 | animation-delay: 1s; 57 | animation-duration: 1.5s; 58 | animation-iteration-count: infinite; 59 | animation-fill-mode: forwards; 60 | animation-timing-function: linear; 61 | } 62 | } 63 | } 64 | 65 | & p { 66 | text-align: justify; 67 | } 68 | 69 | & .tabs { 70 | margin: 0 -30px; 71 | padding: 0 28px; 72 | display: flex; 73 | flex-direction: row; 74 | border-bottom: 1px solid #e6e6e6; 75 | 76 | & .tab { 77 | padding: 5px; 78 | position: relative; 79 | 80 | &:not(.active) { 81 | cursor: pointer; 82 | } 83 | 84 | &.active { 85 | border-bottom: 1px solid orange; 86 | bottom: -1px; 87 | margin-top: -1px; 88 | } 89 | } 90 | } 91 | 92 | & .partner-example { 93 | overflow: hidden; 94 | } 95 | 96 | & pre { 97 | overflow: scroll; 98 | height: 100%; 99 | padding-bottom: 16px; 100 | padding-top: 10px; 101 | box-sizing: border-box; 102 | 103 | .scrollableWithoutScrollbars(both); 104 | } 105 | 106 | & code[class*="language-"], 107 | & pre[class*="language-"] { 108 | text-shadow: none; 109 | border-radius: 3px; 110 | 111 | & .token.operator { 112 | background: none; 113 | } 114 | } 115 | } 116 | } 117 | 118 | @keyframes brue-rotate { 119 | from { 120 | cursor: initial; 121 | color: #ff0000; 122 | filter: hue-rotate(0deg); 123 | } 124 | 125 | to { 126 | cursor: initial; 127 | color: #ff0000; 128 | filter: hue-rotate(360deg); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Field/FieldValue.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PKDateStyle } from "../../../constants"; 3 | import { getCSSFromFieldProps, FieldProperties, FieldTypes } from "./fieldCommons"; 4 | import { SelectableComponent } from "../../sections/useRegistrations"; 5 | import format from "date-fns/format"; 6 | 7 | type ValueProps = Partial> & { 8 | fieldData: Partial>; 9 | }; 10 | 11 | /** 12 | * @TODO use svg text to allow it to resize manually? 13 | */ 14 | 15 | export default function FieldValue(props: ValueProps) { 16 | const { fieldData } = props; 17 | const style = getCSSFromFieldProps(fieldData, "label"); 18 | const parsedValue = getValueFromProps(props); 19 | 20 | return ( 21 | 22 | {parsedValue} 23 | 24 | ); 25 | } 26 | 27 | function getValueFromProps(props: ValueProps) { 28 | const { 29 | fieldData: { value, dateStyle, timeStyle }, 30 | } = props; 31 | const valueAsDate = new Date(value); 32 | 33 | const shouldShowDate = dateStyle !== undefined && dateStyle !== PKDateStyle.None; 34 | const shouldShowTime = timeStyle !== undefined && timeStyle !== PKDateStyle.None; 35 | 36 | if (isNaN(valueAsDate.getTime()) || (!shouldShowTime && !shouldShowDate)) { 37 | /** 38 | * Date parsing failed ("Invalid date"). 39 | * Or it doesn't have to be parsed as date 40 | * We are returning directly the value 41 | * without performing any kind of parsing. 42 | */ 43 | return value; 44 | } 45 | 46 | const timeValues = []; 47 | 48 | if (shouldShowDate) { 49 | timeValues.push(getDateValueFromDateStyle(dateStyle, valueAsDate)); 50 | } 51 | 52 | if (shouldShowTime) { 53 | timeValues.push(getTimeValueFromTimeStyle(timeStyle, valueAsDate)); 54 | } 55 | 56 | return timeValues.join(" "); 57 | } 58 | 59 | function getDateValueFromDateStyle(dateStyle: PKDateStyle, value: Date) { 60 | switch (dateStyle) { 61 | case PKDateStyle.Short: 62 | return format(value, "P"); 63 | case PKDateStyle.Medium: 64 | return format(value, "MMM dd, yyyy"); 65 | case PKDateStyle.Long: 66 | return format(value, "MMMM dd, yyyy"); 67 | case PKDateStyle.Full: 68 | return format(value, "PPPP G"); 69 | default: 70 | return value; 71 | } 72 | } 73 | 74 | function getTimeValueFromTimeStyle(timeStyle: PKDateStyle, value: Date) { 75 | switch (timeStyle) { 76 | case PKDateStyle.Short: 77 | return format(value, "p"); 78 | case PKDateStyle.Medium: 79 | return format(value, "pp"); 80 | case PKDateStyle.Long: 81 | // @TODO Timezone format (PST, GMT) should be added here 82 | return format(value, "h:mm:ss a"); 83 | case PKDateStyle.Full: 84 | // @TODO Timezone format (PST, GMT) as extended string should be added here 85 | return format(value, "h:mm:ss a OOOO"); 86 | default: 87 | return value; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Configurator/ExportModal/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Prism from "prismjs"; 3 | import "./style.less"; 4 | import { PassMixedProps } from "@pkvd/pass"; 5 | import { createClassName } from "../../utils"; 6 | import Modal, { ModalProps } from "../ModalBase"; 7 | import * as Store from "@pkvd/store"; 8 | import * as templates from "../../../partners-templates"; 9 | 10 | /** Defined by Webpack */ 11 | declare const partners: Partner[]; 12 | 13 | /** 14 | * This modal must receive some data to generate, for each "partner" 15 | * the model importing example. 16 | * 17 | * To describe every partner, I need a way to describe code chunks for 18 | * each field, epecially mocked ones. 19 | * 20 | */ 21 | 22 | interface Partner { 23 | showName: string; 24 | lang: string; 25 | templateName: string; 26 | } 27 | 28 | interface Props extends Omit { 29 | passProps: PassMixedProps; 30 | translations: Store.LocalizedTranslationsGroup; 31 | projectOptions: Store.ProjectOptions; 32 | media: Store.LocalizedMediaGroup; 33 | } 34 | 35 | export default function ExportModal(props: Props) { 36 | const [activePartnerTab, setActivePartnerTab] = React.useState(0); 37 | const codeRef = React.useRef(); 38 | 39 | React.useEffect(() => { 40 | // To load line-numbers (for some reason, they don't get loaded 41 | // if element is not loaded with the page) 42 | Prism.highlightElement(codeRef.current); 43 | }, [activePartnerTab]); 44 | 45 | const partnerTabs = partners.map((partner, index) => { 46 | const className = createClassName(["tab"], { 47 | active: index === activePartnerTab, 48 | }); 49 | 50 | return ( 51 |
setActivePartnerTab(index)} key={`tab${index}`}> 52 | {partner.showName} 53 |
54 | ); 55 | }); 56 | 57 | const { templateName, lang } = partners[activePartnerTab]; 58 | const partnerFilledContent = templates[templateName]({ 59 | store: { 60 | pass: props.passProps, 61 | translations: props.translations, 62 | projectOptions: props.projectOptions, 63 | media: props.media, 64 | }, 65 | }); 66 | 67 | const codeLanguage = `language-${lang}`; 68 | 69 | return ( 70 | 71 |

72 | Great Pass! 73 |

74 |

75 | Your model is now being exported. Here below you can see some open source libraries examples 76 | to generate programmatically passes with your mock data compiled. Enjoy! 77 |

78 |
{partnerTabs}
79 |
80 |
81 | 					
87 | 				
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/Configurator/MediaModal/ModalNavigation/style.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../App/common.less"; 2 | 3 | .modal-content#media-collection > #media-collector > header { 4 | border-bottom: 0.5px solid #292929; 5 | 6 | & > nav { 7 | display: flex; 8 | flex-direction: row; 9 | flex-grow: 1; 10 | align-items: center; 11 | /** for ellipsis, hip hip, hooray! **/ 12 | overflow: hidden; 13 | 14 | &.allow-back-nav { 15 | & .back ~ span { 16 | margin-left: 5px; 17 | } 18 | } 19 | 20 | &:not(.allow-back-nav) .back { 21 | opacity: 0; 22 | transition-delay: 0.5s, 0s; 23 | margin-left: -25px; 24 | 25 | & ~ span { 26 | transition-delay: 0.5ss; 27 | margin-left: 0; 28 | } 29 | } 30 | 31 | & svg.back { 32 | transition: all 0.5s ease-in-out, opacity 0.5s ease-in-out 0.5s; 33 | opacity: 1; 34 | transform: rotate(180deg); 35 | fill: #e6e6e6; 36 | height: 20px; 37 | width: 25px; 38 | cursor: pointer; 39 | flex-shrink: 0; 40 | 41 | & ~ span { 42 | transition: margin-left 0.5s ease-in-out 0.3s; 43 | } 44 | } 45 | 46 | & > div { 47 | color: #a4a4a4; 48 | margin: 0; 49 | /** for ellipsis **/ 50 | text-overflow: ellipsis; 51 | overflow: hidden; 52 | white-space: nowrap; 53 | /** end ellipsis **/ 54 | 55 | & span#coll-name { 56 | cursor: pointer; 57 | color: #e6e6e6; 58 | 59 | &::before { 60 | content: "/"; 61 | cursor: initial; 62 | margin: 0 5px; 63 | color: #a4a4a4; 64 | display: inline-block; 65 | vertical-align: -4px; 66 | font-size: 1.7em; 67 | font-weight: 400; 68 | pointer-events: none; 69 | .lookMumIamAppearing(0.5s, ease-in-out); 70 | } 71 | 72 | /** 73 | * Span to contain element and allow 74 | * enlarging focus effect 75 | */ 76 | 77 | & > span { 78 | position: relative; 79 | .lookMumIamAppearing(0.5s, ease-in-out, 0.5s); 80 | 81 | &::after { 82 | content: ""; 83 | position: absolute; 84 | width: 0%; 85 | transition: width 0.4s ease-in-out; 86 | border-bottom: 1px solid #e6e6e6; 87 | left: 0; 88 | bottom: -3px; 89 | } 90 | 91 | &:focus-within::after { 92 | width: 100%; 93 | } 94 | 95 | & > input { 96 | border: 0; 97 | background: transparent; 98 | color: #e6e6e6; 99 | font-size: 1em; 100 | font-family: "SF Display"; 101 | font-weight: 200; 102 | outline: none; 103 | border-radius: 0; 104 | cursor: pointer; 105 | 106 | &::selection { 107 | background-color: #afafaf; 108 | color: #e6e6e6; 109 | } 110 | } 111 | } 112 | } 113 | 114 | & + svg { 115 | .lookMumIamAppearing(0.5s, ease-in-out, 0.5s); 116 | width: 15px; 117 | height: 15px; 118 | flex-shrink: 0; 119 | fill: #e6e6e6; 120 | margin: 0 10px; 121 | cursor: pointer; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Configurator/OptionsBar/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | // visibility by Knut M. Synstad from the Noun Project (heavily edited) 4 | // https://thenounproject.com/term/visibility/873045 5 | 6 | export function EyeVisibleIcon(props: React.SVGProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | // no visibility by Knut M. Synstad from the Noun Project (heavily edited) 16 | // https://thenounproject.com/term/no-visibility/873044 17 | 18 | export function EyeInvisibleIcon(props: React.SVGProps) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export function ShowMoreIcon(props: React.SVGProps) { 29 | return ( 30 | 31 | 35 | 36 | ); 37 | } 38 | 39 | export function TranslationsIcon(props: React.SVGProps) { 40 | return ( 41 | 42 | 43 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passkit Visual Designer 2 | 3 | This is a project that aims to make Apple Wallet design phase easier through a simple but complete user interface. 4 | 5 | ## Designing flow 6 | 7 | While creating an Apple Wallet Pass, the generation is not the step that matters the most, but your idea before creating it. So having an idea of colors, texts, translations and so on is very important, also due to a matter of branding. 8 | 9 | So the designing flow for Apple Wallet passes _passes_ through this: Design, Approval, Generation. 10 | 11 | If, from the perspective of generation, several libraries and proprietary implementations already exist, really a few websites or tools for design already exist... and they are definitely not open-source. 12 | 13 | That's what PKVD was born for: to attempt emulating the behavior of Apple Wallet before the on-device tests happen. 14 | 15 | ## Technical limitations and emulation 16 | 17 | Since what has been created is just an emulation, what happens is that few things might not appear as they really appear on Apple Wallet. If you discover any issues like these, please open an issue by attaching: 18 | 19 | - the image of how it shows up in the website 20 | - the image of how it shows up in Apple Wallet 21 | - The pass itself (without manifest and signature) 22 | 23 | So the behavior can be tested and fixed. 24 | 25 | Some limitations might instead regard the platform the website is running on. One of the examples is color-fidelty. 26 | Apple Wallet uses a color scheme called [Display-P3](https://en.wikipedia.org/wiki/DCI-P3), which is widely available in native applications, [but not yet available in browsers](https://caniuse.com/css-color-function). 27 | For this reason, some colors might result less brighter or just different. 28 | 29 | At the time of writing, only Safari supports this color scheme and, for this reason, it makes no sense to work with it. 30 | 31 | It is also known that, currently, Apple Wallet applies a really small gradient on the background color, which changes color perception and which seems to be different from color to color. I've been able to find no details on how this gradient is composed. 32 | 33 | ## What does expect us the future? 34 | 35 | I don't know what the future reserves for you, but I know for sure which are the ideas that will be implemented in the project: 36 | 37 | - [ ] Offline website (it already doesn't require a connection to work); 38 | - [ ] Apple Watch view mode emulation (requires me to buy an Apple Watch first); 39 | - [ ] Notification server testing with live changes; 40 | - [ ] Application localization; 41 | - [ ] Moar easter eggs (yes, I filled the project with a lot of easter eggs and pop culture references); 42 | 43 | ## Other 44 | 45 | The idea behind this project was born after the first publish of my other library for passes generation, [passkit-generator](https://github.com/alexandercerutti/passkit-generator), in late 2018, but the project building actually take off in early 2020. It took about a year to reach the first publish phase. 46 | 47 | A big thanks to all the people that gave me suggestions to improve this and a big thanks to [Lorella Levantino](https://dribbble.com/lorellalevantino) for the great help she gave me while designing and realizing the UI/UX. 48 | 49 | --- 50 | 51 | Made with ❤ in Italy. Any contribution and feedback is welcome. 52 | -------------------------------------------------------------------------------- /src/PassSelector/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import "./style.less"; 4 | import { PassMixedProps } from "@pkvd/pass"; 5 | import { PassKind } from "../model"; 6 | import PassList from "./PassList"; 7 | import SelectablePass from "./SelectablePass"; 8 | import { getAlternativesByKind } from "./SelectablePass/useAlternativesRegistration"; 9 | import * as Store from "@pkvd/store"; 10 | 11 | // Webpack declared 12 | declare const __DEV__: boolean; 13 | 14 | interface DispatchProps { 15 | setPassProps: typeof Store.Pass.setPropsBatch; 16 | } 17 | 18 | interface SelectorState { 19 | selectedKind: PassKind; 20 | } 21 | 22 | interface SelectorProps extends DispatchProps { 23 | pushHistory(path: string, init?: Function): void; 24 | } 25 | 26 | class PassSelector extends React.PureComponent { 27 | private config = { 28 | introText: "Select your pass model", 29 | }; 30 | 31 | constructor(props: SelectorProps) { 32 | super(props); 33 | 34 | this.state = { 35 | selectedKind: undefined, 36 | }; 37 | 38 | this.onPassSelect = this.onPassSelect.bind(this); 39 | this.onAlternativeSelection = this.onAlternativeSelection.bind(this); 40 | } 41 | 42 | onPassSelect(passProps: PassMixedProps) { 43 | if (__DEV__) { 44 | console.log("Performed selection of", passProps.kind); 45 | } 46 | 47 | if (this.state.selectedKind === passProps.kind) { 48 | this.setState({ 49 | selectedKind: undefined, 50 | }); 51 | 52 | return; 53 | } 54 | 55 | this.setState({ 56 | selectedKind: passProps.kind, 57 | }); 58 | } 59 | 60 | /** 61 | * Receives the pass props that identifies an alternative 62 | * along with the kind and performs page changing 63 | * 64 | * @param passProps 65 | */ 66 | 67 | onAlternativeSelection(passProps: PassMixedProps) { 68 | this.props.pushHistory("/creator", () => this.props.setPassProps(passProps)); 69 | } 70 | 71 | render() { 72 | const { selectedKind } = this.state; 73 | const availableAlternatives = getAlternativesByKind(selectedKind) || []; 74 | 75 | const passes = Object.entries(PassKind).map(([_, pass]) => { 76 | return ; 77 | }); 78 | 79 | const alternativesList = availableAlternatives.map((alternative) => { 80 | return ( 81 | 87 | ); 88 | }); 89 | 90 | const AlternativesListComponent = 91 | (alternativesList.length && ( 92 | 93 | {alternativesList} 94 | 95 | )) || 96 | null; 97 | 98 | return ( 99 |
100 |
101 |

{this.config.introText}

102 |
103 |
104 | 105 | {passes} 106 | 107 | {AlternativesListComponent} 108 |
109 |
110 | ); 111 | } 112 | } 113 | 114 | export default connect(null, { 115 | setPassProps: Store.Pass.setPropsBatch, 116 | })(PassSelector); 117 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { DefinePlugin } = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const forkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 5 | const { version } = require("./package.json"); 6 | const partners = require("./partners-templates/index.json"); 7 | 8 | module.exports = { 9 | mode: process.env.NODE_ENV === "dev" ? "development" : "production", 10 | target: "web", 11 | entry: "./src/public/index.tsx", 12 | output: { 13 | path: path.join(__dirname, "dist"), 14 | filename: "[name].bundle.js", 15 | clean: true, 16 | }, 17 | module: { 18 | rules: [{ 19 | test: /\.jsx?$/, 20 | use: [{ 21 | loader: "thread-loader", 22 | }, { 23 | loader: "babel-loader", 24 | options: { 25 | presets: ["@babel/preset-react"] 26 | } 27 | }], 28 | exclude: /(node_modules)/, 29 | }, { 30 | test: /\.tsx?$/, 31 | use: [{ 32 | loader: "thread-loader", 33 | }, { 34 | loader: "ts-loader", 35 | options: { 36 | happyPackMode: true 37 | } 38 | }] 39 | }, { 40 | test: /\.less$/, 41 | use: [{ 42 | loader: "style-loader" 43 | }, { 44 | loader: "css-loader" 45 | }, { 46 | loader: "less-loader" 47 | }] 48 | }, { 49 | test: /\.css$/, 50 | use: [{ 51 | loader: "style-loader", 52 | }, { 53 | loader: "css-loader" 54 | }] 55 | }, { 56 | test: /\.otf$/, 57 | loader: "file-loader" 58 | }, { 59 | test: /\.(hbs|handlebars)$/, 60 | loader: "handlebars-loader", 61 | options: { 62 | rootRelative: path.resolve(__dirname, "partners-templates"), 63 | helperDirs: [ 64 | path.resolve(__dirname, "partners-templates/helpers") 65 | ], 66 | knownHelpers: [ 67 | "hasContent", 68 | "isDefaultLanguage" 69 | ], 70 | } 71 | }] 72 | }, 73 | optimization: { 74 | splitChunks: { 75 | cacheGroups: { 76 | vendor: { 77 | test: /[\\/]node_modules[\\/].+/, 78 | name: "vendor", 79 | chunks: "initial", 80 | priority: 1, 81 | }, 82 | react: { 83 | test: /[\\/]node_modules[\\/](react|react-dom)[\\/].+/, 84 | name: "react.vendor", 85 | chunks: "initial", 86 | priority: 2, 87 | }, 88 | partners: { 89 | test: /[\\/](partners-templates|node_modules[\\/]handlebars)[\\/].+/, 90 | name: "partners", 91 | priority: 2, 92 | chunks: "initial" 93 | } 94 | }, 95 | } 96 | }, 97 | devtool: "source-map", 98 | resolve: { 99 | extensions: [".js", ".jsx", ".ts", ".tsx"], 100 | alias: { 101 | "@pkvd/pass": path.resolve(__dirname, "./src/Pass/"), 102 | "@pkvd/store": path.resolve(__dirname, "./src/store/") 103 | } 104 | }, 105 | devServer: { 106 | port: 3000, 107 | host: "0.0.0.0", 108 | historyApiFallback: true, 109 | }, 110 | plugins: [ 111 | new HtmlWebpackPlugin({ 112 | title: "Passkit Visual Designer", 113 | template: "./src/public/index.html", 114 | filename: "./index.html", 115 | description: "A web tool to make it easier designing Apple Wallet Passes graphically", 116 | chunks: "all", 117 | }), 118 | new forkTsCheckerWebpackPlugin(), 119 | new DefinePlugin({ 120 | __DEV__: process.env.NODE_ENV === "dev", 121 | partners: JSON.stringify(partners), 122 | version: JSON.stringify(version), 123 | }), 124 | ] 125 | }; 126 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { PassKind } from "../model"; 2 | import { Constants, PassMixedProps, PassMediaProps } from "@pkvd/pass"; 3 | 4 | const { PKTextAlignment, PKTransitType } = Constants; 5 | 6 | /** Webpack defined */ 7 | declare const __DEV__: boolean; 8 | 9 | const __DEV_DEFAULT_PASS_PROPS = __DEV__ 10 | ? { 11 | transitType: PKTransitType.Boat, 12 | kind: PassKind.BOARDING_PASS, 13 | /** FEW TESTING DATA **/ 14 | /** 15 | 16 | backgroundColor: "rgb(255,99,22)", 17 | labelColor: "rgb(0,0,0)", 18 | foregroundColor: "rgb(255,255,255)", 19 | headerFields: [ 20 | { 21 | key: "num", 22 | label: "volo", 23 | value: "EJU996", 24 | textAlignment: PKTextAlignment.Center, 25 | }, 26 | { 27 | key: "date", 28 | label: "Data", 29 | value: "21 set", 30 | textAlignment: PKTextAlignment.Center, 31 | } 32 | ], 33 | primaryFields: [ 34 | { 35 | key: "from", 36 | label: "Venezia Marco Polo", 37 | value: "VCE", 38 | textAlignment: PKTextAlignment.Left, 39 | }, 40 | { 41 | key: "to", 42 | label: "Napoli", 43 | value: "NAP", 44 | textAlignment: PKTextAlignment.Right, 45 | }, 46 | ], 47 | auxiliaryFields: [ 48 | { 49 | key: "user", 50 | label: "Passeggero", 51 | value: "SIG. ALEXANDER PATRICK CERUTTI", 52 | textAlignment: PKTextAlignment.Left, 53 | }, { 54 | key: "seat", 55 | label: "Posto", 56 | value: "1C*", 57 | textAlignment: PKTextAlignment.Center, 58 | } 59 | ], 60 | secondaryFields: [ 61 | { 62 | key: "Imbarco", 63 | label: "Imbarco chiuso", 64 | value: "18:40", 65 | textAlignment: PKTextAlignment.Center, 66 | }, 67 | { 68 | key: "takeoff", 69 | label: "Partenza", 70 | value: "19:10", 71 | textAlignment: PKTextAlignment.Center, 72 | }, 73 | { 74 | key: "SpeedyBoarding", 75 | label: "SB", 76 | value: "Si", 77 | textAlignment: PKTextAlignment.Center, 78 | }, 79 | { 80 | key: "boarding", 81 | label: "Imbarco", 82 | value: "Anteriore", 83 | textAlignment: PKTextAlignment.Center, 84 | } 85 | ] 86 | */ 87 | } 88 | : null; 89 | 90 | export const initialState: State = { 91 | pass: { 92 | ...__DEV_DEFAULT_PASS_PROPS, 93 | }, 94 | media: {}, 95 | translations: {}, 96 | projectOptions: { 97 | activeMediaLanguage: "default", 98 | }, 99 | }; 100 | 101 | export interface State { 102 | pass: Partial; 103 | media: LocalizedMediaGroup; 104 | translations: LocalizedTranslationsGroup; 105 | projectOptions: ProjectOptions; 106 | } 107 | 108 | export interface ProjectOptions { 109 | title?: string; 110 | activeMediaLanguage: string; 111 | id?: string; 112 | savedAtTimestamp?: number; 113 | } 114 | 115 | export interface LocalizedTranslationsGroup { 116 | [languageOrDefault: string]: TranslationsSet; 117 | } 118 | 119 | export interface TranslationsSet { 120 | enabled: boolean; 121 | translations: { 122 | [translationCoupleID: string]: [placeholder?: string, value?: string]; 123 | }; 124 | } 125 | 126 | export interface LocalizedMediaGroup { 127 | [languageOrDefault: string]: MediaSet; 128 | } 129 | 130 | export type MediaSet = { 131 | [K in keyof PassMediaProps]: CollectionSet; 132 | }; 133 | 134 | export interface CollectionSet { 135 | activeCollectionID: string; 136 | enabled: boolean; 137 | collections: { 138 | [collectionID: string]: MediaCollection; 139 | }; 140 | } 141 | 142 | export interface MediaCollection { 143 | name: string; 144 | resolutions: IdentifiedResolutions; 145 | } 146 | 147 | export interface IdentifiedResolutions { 148 | [resolutionID: string]: { 149 | name: string; 150 | content: ArrayBuffer; 151 | }; 152 | } 153 | -------------------------------------------------------------------------------- /src/store/middlewares/PurgeMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction, Dispatch, MiddlewareAPI } from "redux"; 2 | import { PassMediaProps } from "@pkvd/pass"; 3 | import type { State } from ".."; 4 | import * as Store from ".."; 5 | import { CollectionSet, MediaCollection } from "../store"; 6 | 7 | export default function PurgeMiddleware(store: MiddlewareAPI) { 8 | return (next: Dispatch) => (action: Store.Options.Actions.Set) => { 9 | if (action.type !== Store.Options.SET_OPTION || action.key !== "activeMediaLanguage") { 10 | return next(action); 11 | } 12 | 13 | const { 14 | media: prepurgeMedia, 15 | projectOptions: { activeMediaLanguage }, 16 | translations, 17 | } = store.getState(); 18 | 19 | if (action.type === Store.Options.SET_OPTION) { 20 | const mediaGenerator = purgeGenerator(store, Store.Media.Purge); 21 | const translationsGenerator = purgeGenerator(store, Store.Translations.Destroy); 22 | 23 | /** Generators startup */ 24 | mediaGenerator.next(); 25 | translationsGenerator.next(); 26 | 27 | /** 28 | * If language changed, we have to discover if 29 | * current language has medias and translations that can be purged. 30 | * 31 | * Language might not have been initialized. In that case we cannot 32 | * purge anything nor destroy. 33 | */ 34 | 35 | if (prepurgeMedia[activeMediaLanguage]) { 36 | for (const [mediaName, collectionSet] of Object.entries( 37 | prepurgeMedia[activeMediaLanguage] 38 | ) as [keyof PassMediaProps, CollectionSet][]) { 39 | enqueueMediaPurgeOnEmpty(mediaGenerator, collectionSet, activeMediaLanguage, mediaName); 40 | } 41 | 42 | const nextState = mediaGenerator.next().value; 43 | 44 | if (!Object.keys(nextState.media[activeMediaLanguage]).length) { 45 | store.dispatch(Store.Media.Destroy(activeMediaLanguage)); 46 | } 47 | } else { 48 | // finishing anyway to not leave it suspended... poor generator. 49 | mediaGenerator.next(); 50 | } 51 | 52 | for (const [language, translationSet] of Object.entries(translations)) { 53 | if (!Object.keys(translationSet.translations).length) { 54 | translationsGenerator.next([language]); 55 | } 56 | } 57 | 58 | // Completing. No need to get next state 59 | translationsGenerator.next(); 60 | } 61 | 62 | return next(action); 63 | }; 64 | } 65 | 66 | function areAllCollectionsEmpty(collectionsEntry: [string, MediaCollection][]) { 67 | return collectionsEntry.every(([_, collection]) => Object.keys(collection.resolutions).length); 68 | } 69 | 70 | function enqueueMediaPurgeOnEmpty( 71 | generator: ReturnType, 72 | collectionSet: Store.CollectionSet, 73 | language: string, 74 | mediaName: keyof PassMediaProps 75 | ): void { 76 | const collectionEntries = Object.entries(collectionSet.collections); 77 | 78 | if (!collectionEntries.length || areAllCollectionsEmpty(collectionEntries)) { 79 | generator.next([language, mediaName]); 80 | } 81 | } 82 | 83 | type GeneratorElement = [setID: string, mediaID?: keyof PassMediaProps]; 84 | 85 | function* purgeGenerator( 86 | store: MiddlewareAPI, 87 | actionFactory: (...args: any[]) => AnyAction 88 | ): Generator { 89 | const purgeIdentifiers: GeneratorElement[] = []; 90 | let value: typeof purgeIdentifiers[0]; 91 | 92 | while ((value = yield) !== undefined) { 93 | purgeIdentifiers.push(value); 94 | } 95 | 96 | for (let i = purgeIdentifiers.length, set: typeof value; (set = purgeIdentifiers[--i]); ) { 97 | store.dispatch(actionFactory(...set)); 98 | } 99 | 100 | return store.getState(); 101 | } 102 | -------------------------------------------------------------------------------- /src/Pass/layouts/components/Barcodes/code128.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default () => ( 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | -------------------------------------------------------------------------------- /src/Pass/style.less: -------------------------------------------------------------------------------- 1 | .pass { 2 | border-radius: 7px; 3 | width: 100%; 4 | 5 | /** 6 | * Actually these sizes have been took 7 | * from a resized model of Apple Wallet pass 8 | * found somewhere on the web 9 | **/ 10 | 11 | max-width: 230px; 12 | height: 369.59px; 13 | 14 | position: relative; 15 | transition: box-shadow 0.5s; 16 | 17 | & .card { 18 | display: flex; 19 | width: 100%; 20 | height: 100%; 21 | border-radius: inherit; 22 | 23 | /* For backflip */ 24 | transition: transform 1s; 25 | transform-origin: center; 26 | transform-style: preserve-3d; 27 | 28 | &.show-back { 29 | transform: rotateY(180deg); 30 | 31 | & > .content { 32 | z-index: -1; 33 | } 34 | 35 | & > .back-fields { 36 | z-index: 99; 37 | } 38 | } 39 | 40 | & .decorations { 41 | overflow: hidden; 42 | position: absolute; 43 | top: 0; 44 | right: 0; 45 | left: 0; 46 | bottom: 0; 47 | z-index: 1; 48 | pointer-events: none; 49 | } 50 | 51 | & > .content { 52 | width: 100%; 53 | height: 100%; 54 | padding: 10px; 55 | box-sizing: border-box; 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | border-radius: inherit; 60 | 61 | /* For Backflip */ 62 | position: absolute; 63 | -webkit-backface-visibility: hidden; 64 | backface-visibility: hidden; 65 | 66 | /** Background Image **/ 67 | overflow: hidden; 68 | 69 | &::after { 70 | content: ""; 71 | position: absolute; 72 | top: 0px; 73 | left: 0; 74 | right: 0; 75 | z-index: -1; 76 | bottom: 0; 77 | background: var(--pass-background); 78 | background-size: cover; 79 | background-position: bottom center; 80 | } 81 | 82 | &.bg-image::after { 83 | filter: blur(6px); 84 | } 85 | } 86 | } 87 | } 88 | 89 | .pass[data-kind="boardingPass"] { 90 | & .decorations { 91 | &::before, 92 | &::after { 93 | content: ""; 94 | width: 10px; 95 | height: 10px; 96 | position: absolute; 97 | top: 112px; 98 | border-radius: 50%; 99 | z-index: 999; 100 | transition: opacity 0.5s; 101 | } 102 | 103 | &::before { 104 | left: -6px; 105 | background: linear-gradient(to left, #333 0%, #333 70%); 106 | } 107 | 108 | &::after { 109 | right: -6px; 110 | background: linear-gradient(to right, #333 0%, #333 70%); 111 | } 112 | } 113 | } 114 | 115 | .pass[data-kind="eventTicket"] { 116 | & .card { 117 | & .content { 118 | padding-top: 15px; 119 | } 120 | } 121 | 122 | & .decorations { 123 | &::before { 124 | content: ""; 125 | width: 40px; 126 | height: 28px; 127 | position: absolute; 128 | top: -19px; 129 | left: calc(50% - 20px); 130 | background: linear-gradient(to top, #333 25%, #000 100%); 131 | border-radius: 50%; 132 | z-index: 999; 133 | } 134 | } 135 | } 136 | 137 | .pass[data-kind="coupon"] { 138 | border-radius: 0px; 139 | 140 | & .card { 141 | & .content { 142 | border-radius: unset; 143 | } 144 | } 145 | 146 | & .decorations { 147 | &::before, 148 | &::after { 149 | content: ""; 150 | width: 100%; 151 | height: 6px; 152 | position: absolute; 153 | background-size: 7px 5px; 154 | background-repeat: repeat-x; 155 | background-color: var(--pass-background); 156 | } 157 | 158 | &::before { 159 | top: -3px; 160 | background-image: linear-gradient(140deg, #333 50%, transparent 50%), 161 | linear-gradient(220deg, #333 50%, transparent 50%); 162 | background-position: top left, top left; 163 | } 164 | 165 | &::after { 166 | bottom: -3px; 167 | background-image: linear-gradient(40deg, #222222 50%, transparent 50%), 168 | linear-gradient(-40deg, #222222 50%, transparent 50%); 169 | background-position: bottom left, bottom left; 170 | } 171 | } 172 | } 173 | --------------------------------------------------------------------------------