prototyping portfolio
Born Ukraine, live in the UK with lovely 2 kids and wife
20+ years of design experience
Prototyping is passion
Fan of coding in general, react hooks still confuses me
I am building design community in London focused around design tools and prototyping.
Running Framer, Figma events for 4 years, expanding to graphics design and VR.
Started my Framer journey with some SVG drawing and arrays
First time got my hands on API's - Firebase and Spotify
Wrote spotifyFramer module - https://github.com/mamezito-zz/spotifyApiFramer
#spotify API
# this finds our albums
exports.searchAlbums = (query) ->
bla=8
r = new XMLHttpRequest
qString = "?q=" + encodeURIComponent(query) + "&type=album"
r.open 'GET', 'https://api.spotify.com/v1/search' + qString, false
r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
album=[]
r.onreadystatechange = ->
if r.readyState != 4 or r.status != 200
return
response = JSON.parse(r.responseText)
exports.albums = response.albums
r.send()
# this gets a specific track
exports.fetchTracks = (albumId) ->
r = new XMLHttpRequest
r.open 'GET', 'https://api.spotify.com/v1/albums/' + albumId, false
r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
r.onreadystatechange = ->
if r.readyState != 4 or r.status != 200
return
response = JSON.parse(r.responseText)
exports.tracks = response.tracks.items
# print tracks
# music.video = track.preview_url
# artist.html = track.artists[0].name
r.send()
First time using hardware prototyping and node.js
head.on "change:x", ->
rotation=dragOnCircle.dragAngle
firebase.patch("/sphero", {"rotation": rotation})
colorKnob.on "change:x", ->
hue=dragOnCircle.dragAngle
colorHSL=new Color("hsl(#{hue}, 100, 50)")
colorKnob.backgroundColor=colorHSL
hex=colorHSL.toHexString()firebase.patch("/sphero", {"color": hex})
Worked on Framer module to connect Framer with Mapbox API.
Prototype itself relies on Google Maps Search and Direction API
Particle.io hardware, webhooks, Phillips Hue bridge API
Created module to connect framer to API.ai (dialogflow), module to create messenger chatbots
#framer info
Framer.Info =
title: "Pizza hut facebook messenger bot"
description: "api.ai module, ios kit module, fb messenger bot module"
author: "Sergey Voronov"
twitter: "mamezito"
#modules
chatBot = require "chatBot"
api_ai = require 'apiai'
#change token to your token here
token = "e0da974e397747a58fbd7683013cbf8f"
session=Utils.randomNumber(0, 100000)
#initial settings
botName="Pizza Hut"
botImage="https://qph.ec.quoracdn.net/main-qimg-71867419a7825d58bdbb7b2a1b4605cd-c?convert_to_webp=true"
likes="282k people like this"
botCategory="Food delivery"
user="Sergiy"
chatBot.createMessenger(botName,botImage,likes,botCategory,user)
#callback function that uses response from api.ai
sendfunc=(data)->
msg=new chatBot.Message
type:"botMsg"
text:data.result.speech
print data
#bot logic
#function checking for user input
window["userInput"]=(input)->
#we are sending users input as string to api.ai
#using sendfunc as callback when response with data is ready
api_ai.send input,sendfunc, token, session
Framer X prototyping of onboarding experience for swedish fintech
import * as React from "react"
import { useState, useEffect } from "react"
import { Frame, addPropertyControls, ControlType } from "framer"
import { NumberKey, BackKey } from "./canvas"
const keys = [
"1", "2", "3", "4","5","6", "7", "8","9","0","00","backspace",
]
export function Keyboard({ gap, background, highlight, value, onValueChange }) {
function updateValue(key) {
const val =
key !== "backspace"
? value + key
: value.toString().substring(0, value.toString().length - 1)
const number = Number.parseFloat(val)
onValueChange(number ? number : 0)
}
return (
<Frame
style={{ display: "flex", flexWrap: "wrap", alignItems: "center" }}
backgroundColor={background}
size="100%"
>
{keys.map(key => (
<Frame
style={{
position: "relative",
marginLeft: gap,
marginTop: gap,
background: background,
height: 48,
width: `calc((100% - ${gap}px * 4)/3)`,
borderRadius: 4,
}}
whileTap={{
background: highlight,
}}
onTap={() => updateValue(key)}
key={key}
>
{key !== "backspace" ? (
<NumberKey value={key} center={true} />
) : (
<BackKey size={32} center={true} />
)}
</Frame>
))}
</Frame>
)
}
Keyboard.defaultProps = {
value: "",
height: 128,
width: 240,
gap: 6,
background: "white",
highlight: "#ccc",
onValueChange(value: number) {},
}
addPropertyControls(Keyboard, {
value: {
title: "Value",
type: ControlType.String,
defaultValue: "",
},
gap: {
title: "Gap",
type: ControlType.Number,
defaultValue: 6,
},
background: {
title: "Background",
type: ControlType.Color,
defaultValue: "#0099ff",
},
highlight: {
title: "Highlight",
type: ControlType.Color,
defaultValue: "#0099ff",
},
})
Working on smart tariff - lots of real data visualisation
Building Figma ui kit for design system
Building Framer X package for design system
Figma to React plugin
import * as React from "react";
import { PropertyControls, ControlType } from "framer";
import { Dropdown } from "@bulb/design/modules/Dropdown";
interface Props {
name: string;
color: string;
width: number;
height: number;
errorMessage:string;
options: [];
status:string;
}
export class DropDown extends React.Component {
static defaultProps = {
status:"unknown"
};
static propertyControls: PropertyControls<Props> = {
label: { type: ControlType.String, title: "Label" },
errorMessage: { type: ControlType.String, title: "Error Message" },
options: {
type: ControlType.Array,
propertyControl: { type: ControlType.String },
defaultValue: ["example"]
},
status: {
type: ControlType.SegmentedEnum,
title: "Status",
options: ["unknown", "invalid", "valid"],
optionTitles: ["default", "error", "success"]
}
};
render() {
const { status, label, errorMessage, options, labelHeading } = this.props;
return (
<Dropdown label={label} status={status} onChange={(e) => console.log(e.target.value)} errorMessage={errorMessage}>
{options.map(o => <option>{o}</option>)} </Dropdown>
);
}
}
// This file holds the main code for the plugins. It has access to the *document*.
// You can access browser APIs in the <script> tag inside "ui.html" which has a
// full browser environment (see documentation).
// This shows the HTML page in "ui.html".
figma.showUI(__html__, { width: 350, height: 250 });
figma.ui.onmessage = (msg: MessageRequest) => {
const message = getMessage(msg.type);
return figma.ui.postMessage(message);
};
/**
* Messages
*/
function getMessage(type: MessageRequestType): MessageResponse {
switch (type) {
case "get-react-code":
const node = getNode();
if (node) {
const code = getComponentCode(node);
if (code) {
return {
type: "success",
message: "Code delivered",
payload: { code }
};
}
}
return {
type: "error",
message: `Sorry, we couldn't generate code for that selection`,
payload: null
};
case "clear":
return {
type: "clear",
message: "Clear the code please",
payload: null
};
default:
return {
type: "error",
message: `Something went wrong`,
payload: null
};
}
}
/**
* Node traversal
*/
function validNodes(selection) {
return selection.filter((sel: SceneNode) => {
// TODO: more robust filter for radio group
return (
sel.type === "INSTANCE" ||
(sel.type === "GROUP" && sel.name.includes("radio-group"))
);
});
}
function getNode(): (InstanceNode & ChildrenMixin) | null {
const selection: (InstanceNode & ChildrenMixin)[] = validNodes(
figma.currentPage.selection
);
// For now just getting the first;
// will probably need something more sophisticated eventually
return (selection.length && selection[0]) || null;
}
/**
* Component code
*/
function getComponentCode(node): string | null {
const text = getText(node);
const { name: instanceName } = node;
const [componentName, type] = instanceName.split("_");
const componentFn = getComponentFn(componentName);
return componentFn({ text, type, node });
}
function getComponentFn(
componentName: string
): ({ text, type }: ComponentProps) => string | null {
return (
componentMap[componentName] ||
(() => {
return null;
})
);
}
/**
* Component definitions
*/
// TODO - for now we can inline these, but if we use this a lot
// and/or if components change a lot, worth looking at finding a way to
// pull the data directly from Solar.
// For now, we should indicate in the `design` repo when we've added a component here,
// to be sure that people update this info when component structure changes in `design`.
const componentMap = {
"CTA-Button": function CtaButton({ text, type }: ComponentProps): string {
return `<CtaButton purpose="${getPurpose(type)}">${text}</CtaButton>`;
},
"CTA-link": function CtaLink({ text, type }: ComponentProps): string {
return `<CtaLink purpose="${getPurpose(type)}">${text}</CtaLink>`;
},
Pathway: function Pathway({ text, type }: ComponentProps): string {
return `<Pathway purpose="${getPurpose(type)}">${text}</Pathway>`;
},
"check-box": function Checkbox({ text, type }: ComponentProps): string {
return `<Checkbox${isChecked(type)} label="${text}"/>`;
},
"radio-group": function RadioButtons({ type, node }: ComponentProps): string {
const options = getOptions(node);
const selected = options.find(o => o.selected === true);
const value = selected ? selected.value : "";
const possibleValues = JSON.stringify(
options.map(({ title, value }) => ({ title, value }))
);
return `<RadioButtons
${value && `value="${value}"`}
label=""
variant="${getPurpose(type)}"
possibleValues={${possibleValues}}
id="[your-id]"
status="unknown"
answerQuestion={()=>{/** Your function **/}} />`;
},
icon: function SvgIcon({ type, node }: ComponentProps): string {
const color = getColor(node);
return `<SvgIcon name="${type}"${color &&
` color={palette.brand.${color}}`} />`;
}
};
function getOptions(node: ComponentNode & ChildrenMixin): Option[] {
return node
.findAll(node => node.type === "INSTANCE")
.map((node: ComponentNode & ChildrenMixin) => {
const { name: instanceName } = node;
const [, type] = instanceName.split("_");
const title = getText(node);
return {
selected: type === "selected",
title,
value: toKebabCase(title)
};
});
}
function getColor(node: ComponentNode & ChildrenMixin) {
// TODO convert fill Id to fill Name
return "blue";
}
function getPurpose(type: string): string {
const map = {
outline: "secondary"
};
return map[type] || type;
}
function isChecked(type) {
return type === "selected" ? " checked" : "";
}
/**
* Text nodes
*/
function getTextNode(node: ChildrenMixin): TextNode {
return node.findOne(
node => node.type === "TEXT" && node.characters.length > 0
) as TextNode;
}
function getText(node: ComponentNode & ChildrenMixin): string | null {
const textNode = getTextNode(node);
if (!textNode) return null;
return textNode.characters.trim();
}
/**
* Types
*/
type MessageRequestType = "get-react-code" | "clear";
type MessageResponseType = "success" | "error" | "clear";
interface MessageResponse {
type: MessageResponseType;
message: string;
payload: { code: string };
}
interface MessageRequest {
type: MessageRequestType;
}
type ComponentTypes = "CTA-Button" | "CTA-link" | "Pathway";
interface ComponentProps {
text: string;
type: string;
node?: ComponentNode & ChildrenMixin;
}
interface Option {
title: string;
value: string;
selected: boolean;
}
/**
* Utils
*/
function toKebabCase(str) {
return (
str &&
str
.match(
/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
)
.map(x => x.toLowerCase())
.join("-")
);
}
Prototyping contract for startup incubator
Framer classic prototyping contract
Design system and product code
Spare time learning 3d
Fascinated by AR and at some point want to explore VR space
Thank you :)