Sergiy Voronov

prototyping portfolio

About me

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

Projector community

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.

Android unlock screen

Started my Framer journey with some SVG drawing and arrays

Spotify cross-device

First time got my hands on API's - Firebase and Spotify

#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()

Droid companion

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})

Google maps

Worked on Framer module to connect Framer with Mapbox API.

 

Prototype itself relies on Google Maps Search and Direction API

Smarthome

Particle.io hardware, webhooks, Phillips Hue bridge API

Chatbots

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

Qapital

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",
    },
})

Bulb

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("-")
  );
}

500labs

Prototyping contract for startup incubator

Facebook

Framer classic prototyping contract

GFK

Design system and product code

3d - blender

Spare time learning 3d

Spark ar and Unity

Fascinated by AR and at some point want to explore VR space

Thank you :)

Prototyping portfolio

By Sergey Voronov

Prototyping portfolio

  • 285