= 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 |
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 |
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 |
16 | );
17 | }
18 |
19 | export function DeleteFieldIcon(props: React.SVGProps) {
20 | return (
21 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
25 | );
26 | }
27 |
28 | export function ShowMoreIcon(props: React.SVGProps) {
29 | return (
30 |
36 | );
37 | }
38 |
39 | export function TranslationsIcon(props: React.SVGProps) {
40 | return (
41 |
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 |
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 |
--------------------------------------------------------------------------------