├── packages
├── app
│ ├── .gitignore
│ ├── public
│ │ ├── robots.txt
│ │ ├── favicon.ico
│ │ ├── icon-32x32.png
│ │ ├── apple-192x192.png
│ │ ├── icon-192x192.png
│ │ ├── icon-512x512.png
│ │ ├── social-share.png
│ │ ├── manifest.json
│ │ └── index.html
│ ├── src
│ │ ├── react-app-env.d.ts
│ │ ├── index.css
│ │ ├── icons
│ │ │ ├── trash.svg
│ │ │ ├── ban.svg
│ │ │ ├── download.svg
│ │ │ ├── random.svg
│ │ │ └── byzenika.svg
│ │ ├── index.tsx
│ │ ├── components
│ │ │ ├── UpdateApp.tsx
│ │ │ ├── Notification.tsx
│ │ │ ├── AssetButton.module.css
│ │ │ ├── AssetButton.tsx
│ │ │ └── Notification.module.css
│ │ ├── duck.ts
│ │ ├── useAssets.ts
│ │ ├── useServiceWorker.tsx
│ │ ├── App.module.css
│ │ └── App.tsx
│ ├── README.md
│ ├── tsconfig.json
│ └── package.json
├── functions
│ ├── .gitignore
│ ├── package.json
│ ├── svg
│ │ ├── svg.js
│ │ ├── svgBuilder.js
│ │ └── svgBuilder.test.js
│ ├── __mocks__
│ │ └── fs.js
│ └── README.md
└── assets
│ ├── .gitignore
│ ├── design
│ ├── hero.png
│ ├── my-zenikanard.afdesign
│ └── icons-my-zenikanard.afdesign
│ ├── src
│ ├── shapes
│ │ ├── floor.svg
│ │ ├── hair-4.svg
│ │ ├── body-tatoo-1.svg
│ │ ├── head.svg
│ │ ├── hat-1.svg
│ │ ├── eye-1.svg
│ │ ├── eye-4.svg
│ │ ├── hat-3.svg
│ │ ├── mouth-3.svg
│ │ ├── hat-6.svg
│ │ ├── glasses-4.svg
│ │ ├── eye-3.svg
│ │ ├── mouth-2.svg
│ │ ├── glasses-3.svg
│ │ ├── hat-7.svg
│ │ ├── face-4.svg
│ │ ├── wear-3.svg
│ │ ├── body.svg
│ │ ├── wear-2.svg
│ │ ├── glasses-5.svg
│ │ ├── hair-3.svg
│ │ ├── hait-3.svg
│ │ ├── hair-1.svg
│ │ ├── eye-2.svg
│ │ ├── mouth-4.svg
│ │ ├── hat-2.svg
│ │ ├── hat-9.svg
│ │ ├── hat-4.svg
│ │ ├── wear-1.svg
│ │ ├── hair-5.svg
│ │ ├── hat-8.svg
│ │ ├── wear-6.svg
│ │ └── wear-5.svg
│ ├── icons
│ │ ├── floor.svg
│ │ ├── hair-4.svg
│ │ ├── body-tatoo-1.svg
│ │ ├── head.svg
│ │ ├── hat-1.svg
│ │ ├── eye-1.svg
│ │ ├── eye-4.svg
│ │ ├── mouth-3.svg
│ │ ├── hat-3.svg
│ │ ├── hat-6.svg
│ │ ├── glasses-4.svg
│ │ ├── mouth-2.svg
│ │ ├── eye-3.svg
│ │ ├── hat-7.svg
│ │ ├── glasses-3.svg
│ │ ├── face-4.svg
│ │ ├── wear-3.svg
│ │ ├── body.svg
│ │ ├── glasses-5.svg
│ │ ├── wear-2.svg
│ │ ├── hair-1.svg
│ │ ├── eye-2.svg
│ │ ├── hair-3.svg
│ │ ├── mouth-4.svg
│ │ ├── hat-2.svg
│ │ ├── hat-9.svg
│ │ ├── hat-4.svg
│ │ ├── wear-1.svg
│ │ ├── hair-5.svg
│ │ ├── hat-8.svg
│ │ ├── wear-6.svg
│ │ ├── wear-5.svg
│ │ └── hait-3.svg
│ └── layers.json
│ ├── package.json
│ ├── README.md
│ └── generate.js
├── .prettierrc
├── .gitignore
├── .gitpod
└── automations.yaml
├── .github
├── workflows
│ └── build.yml
├── dependabot.yml
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
├── .devcontainer
└── devcontainer.json
├── proposals
└── README.md
├── package.json
└── README.md
/packages/app/.gitignore:
--------------------------------------------------------------------------------
1 | src/assets/
--------------------------------------------------------------------------------
/packages/functions/.gitignore:
--------------------------------------------------------------------------------
1 | svg/assets/
--------------------------------------------------------------------------------
/packages/assets/.gitignore:
--------------------------------------------------------------------------------
1 | components/
2 | index.js
--------------------------------------------------------------------------------
/packages/app/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/packages/app/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'save-svg-as-png';
--------------------------------------------------------------------------------
/packages/app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/favicon.ico
--------------------------------------------------------------------------------
/packages/assets/design/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/assets/design/hero.png
--------------------------------------------------------------------------------
/packages/app/public/icon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/icon-32x32.png
--------------------------------------------------------------------------------
/packages/app/public/apple-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/apple-192x192.png
--------------------------------------------------------------------------------
/packages/app/public/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/icon-192x192.png
--------------------------------------------------------------------------------
/packages/app/public/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/icon-512x512.png
--------------------------------------------------------------------------------
/packages/app/public/social-share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/app/public/social-share.png
--------------------------------------------------------------------------------
/packages/assets/design/my-zenikanard.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/assets/design/my-zenikanard.afdesign
--------------------------------------------------------------------------------
/packages/assets/design/icons-my-zenikanard.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zenika-open-source/my-zenikanard/HEAD/packages/assets/design/icons-my-zenikanard.afdesign
--------------------------------------------------------------------------------
/packages/assets/src/shapes/floor.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/floor.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pimpmyduck/functions",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "test": "jest"
7 | },
8 | "devDependencies": {
9 | "jest": "^30.2.0"
10 | },
11 | "dependencies": {
12 | "react-scripts": "5.0.1"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/functions/svg/svg.js:
--------------------------------------------------------------------------------
1 | const svgBuilder = require('./svgBuilder')
2 |
3 | exports.handler = async (event) => {
4 | const params = event.queryStringParameters
5 | return {
6 | statusCode: 200,
7 | headers: {
8 | 'Content-disposition': 'attachment; filename=zenikanard.svg',
9 | 'Content-Type': 'image/svg+xml',
10 | },
11 | body: svgBuilder.build(params),
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/app/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Paytone+One&display=swap');
2 |
3 | body {
4 | margin: 0;
5 | font-family: 'Paytone One';
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | button {
11 | font-family: 'Paytone One';
12 | }
13 |
14 | *,
15 | :before,
16 | :after {
17 | margin: 0;
18 | box-sizing: border-box;
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 | .cache
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.gitpod/automations.yaml:
--------------------------------------------------------------------------------
1 | tasks:
2 | yarn-start:
3 | command: yarn run start
4 | dependsOn:
5 | - install-build
6 | description: yarn-start
7 | name: 'Task 2 : yarn-start'
8 | triggeredBy:
9 | - postDevcontainerStart
10 | install-build:
11 | command: yarn install && yarn run build
12 | description: install-build
13 | name: 'Task 1: install build'
--------------------------------------------------------------------------------
/packages/app/README.md:
--------------------------------------------------------------------------------
1 | # App
2 |
3 | The main web app deployed on [Netlify](https://www.netlify.com/).
4 |
5 | - Bootstraped with [`create-react-app`](https://create-react-app.dev/)
6 | - Written with Typescript
7 |
8 | ## Usage
9 |
10 | From the root folder of the monorepo:
11 |
12 | ```sh
13 | yarn install
14 | yarn start
15 | ```
16 |
17 | ## Build
18 |
19 | From the `packages/app` folder:
20 |
21 | ```sh
22 | yarn build
23 | ```
24 |
--------------------------------------------------------------------------------
/packages/functions/__mocks__/fs.js:
--------------------------------------------------------------------------------
1 | // __mocks__/fs.js
2 | const path = require('path');
3 |
4 | const fs = jest.createMockFromModule('fs');
5 |
6 | fs.__mockFiles = {};
7 |
8 | function readFileSync(filepath) {
9 | return fs.__mockFiles[filepath];
10 | }
11 |
12 | function existsSync(filepath) {
13 | return !!fs.__mockFiles[filepath];
14 | }
15 |
16 | fs.readFileSync = readFileSync;
17 | fs.existsSync = existsSync;
18 |
19 | module.exports = fs;
--------------------------------------------------------------------------------
/packages/app/src/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/app/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import './index.css'
5 | import { ServiceWorkerProvider } from './useServiceWorker'
6 | import * as serviceWorker from './sw'
7 | import App from './App'
8 |
9 | import 'pwacompat'
10 |
11 | serviceWorker.unregisterOnUncatchError()
12 |
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | )
19 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | app:
11 | name: Build and Test App
12 | runs-on: ubuntu-latest
13 | env:
14 | CI: true
15 | steps:
16 | - uses: actions/checkout@v5
17 | - uses: actions/setup-node@v5
18 | with:
19 | node-version: 22
20 | cache: "yarn"
21 | - run: yarn install --frozen-lockfile
22 | - run: yarn test
23 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My Zenikanard",
3 | "image": "node:22.20-slim",
4 | "features": {
5 | "ghcr.io/devcontainers/features/node:1": {
6 | "version": "lts"
7 | }
8 | },
9 | "customizations": {
10 | "vscode": {
11 | "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
12 | "settings": {
13 | "editor.formatOnSave": true
14 | }
15 | },
16 | "jetbrains": {
17 | "plugins": ["com.wix.eslint", "intellij.prettierJS"]
18 | }
19 | },
20 | "forwardPorts": [3000]
21 | }
--------------------------------------------------------------------------------
/packages/app/src/icons/ban.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pimpmyduck/assets",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "add:asset": "yarn optimize && yarn generate && yarn copy:app && yarn copy:functions",
7 | "optimize": "npx svgo -r -f ./src",
8 | "generate": "node generate.js",
9 | "copy:app": "cp -R ./src/ ../app/src/assets/",
10 | "copy:functions": "cp -R ./src/ ../functions/svg/assets/",
11 | "postinstall": "yarn generate && yarn copy:app && yarn copy:functions"
12 | },
13 | "dependencies": {
14 | "lodash": "^4.17.21"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/hair-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/hair-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/assets/src/layers.json:
--------------------------------------------------------------------------------
1 | [
2 | { "id": "floor" },
3 | { "id": "body" },
4 | { "id": "body-tatoo", "name": "Body tatoo", "categoryOrder": 7 },
5 | { "id": "wear", "name": "Wears", "categoryOrder": 6 },
6 | { "id": "acc", "name": "Accessories", "categoryOrder": 8 },
7 | { "id": "head" },
8 | { "id": "face", "name": "Faces", "categoryOrder": 5 },
9 | { "id": "eye", "name": "Eyes", "categoryOrder": 0 },
10 | { "id": "hair", "name": "Hair", "categoryOrder": 2 },
11 | { "id": "glasses", "name": "Glasses", "categoryOrder": 1 },
12 | { "id": "mouth", "name": "Mouth", "categoryOrder": 4 },
13 | { "id": "hat", "name": "Hats", "categoryOrder": 3 }
14 | ]
15 |
--------------------------------------------------------------------------------
/packages/app/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pimp My Duck",
3 | "short_name": "Pimp My Duck",
4 | "description": "Customize your own Zenikanard by Zenika",
5 | "start_url": ".",
6 | "theme_color": "#333333",
7 | "background_color": "#ffffff",
8 | "display": "standalone",
9 | "icons": [
10 | {
11 | "src": "icon-32x32.png",
12 | "sizes": "32x32",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "icon-192x192.png",
17 | "sizes": "192x192",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "icon-512x512.png",
22 | "sizes": "512x512",
23 | "type": "image/png"
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/app/src/components/UpdateApp.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { useServiceWorker } from '../useServiceWorker'
4 | import Notification from './Notification'
5 |
6 | const UpdateApp = () => {
7 | const { isRegisterSucceed, isUpdateAvailable, update } = useServiceWorker()
8 |
9 | return (
10 | <>
11 | {isRegisterSucceed && (
12 | "Pimp my duck" works offline!
13 | )}
14 | {isUpdateAvailable && (
15 |
16 | A new version of "Pimp my duck" is available!
17 |
18 | )}
19 | >
20 | )
21 | }
22 |
23 | export default UpdateApp
24 |
--------------------------------------------------------------------------------
/proposals/README.md:
--------------------------------------------------------------------------------
1 | # How to propose an asset to "Pimp My Duck" ?
2 |
3 | To propose a new asset you have to create a PR with :
4 | - The asset as SVG file into the `/proposals` folder
5 | - Give the asset name and category into the PR description
6 |
7 | Here is the category list:
8 | - Eyes
9 | - Glasses
10 | - Hair
11 | - Hats
12 | - Mouth
13 | - Faces
14 | - Wears
15 | - Body tatoo
16 | - Accessories
17 |
18 | The asset can be built using the [Zenikanard template](./zenikanard_template.svg) SVG file as reference.
19 |
20 | The final asset SVG file must NOT contain the Zenikanard, it's just a template to help the design.
21 |
22 | After the PR, reviewed and merged. A maintainer will integrate it into "Pimp My Duck".
23 |
--------------------------------------------------------------------------------
/packages/functions/README.md:
--------------------------------------------------------------------------------
1 | # Functions
2 |
3 | Endpoints deployed as [Netlify functions](https://www.netlify.com/products/functions/).
4 |
5 | ## Generate SVG File
6 |
7 | ### Endpoint
8 |
9 | ```
10 | GET /.netlify/functions/svg
11 | ```
12 |
13 | Returns the SVG file with response:
14 |
15 | ```
16 | statusCode: 200
17 | headers: {
18 | 'Content-disposition': 'attachment; filename=zenikanard.svg',
19 | 'Content-Type': 'image/svg+xml',
20 | }
21 | ```
22 |
23 | ### Query parameters
24 |
25 | - Parameter `keys` are `layer ids`.
26 | - Parameter `values` are `asset ids`.
27 |
28 | **Example:**
29 |
30 | ```
31 | curl https://pimpmyduck.zenika.com/.netlify/functions/svg?body-tatoo=body-tatoo-1&eye=eye-1&mouth=mouth-1&hair=hair-1
32 | ```
33 |
--------------------------------------------------------------------------------
/packages/app/src/icons/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/functions/svg/svgBuilder.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 |
3 | const layers = require('./assets/layers.json')
4 |
5 | function build(params) {
6 | let svg = ''
7 | layers.forEach((layer) => {
8 | // body and head layers
9 | if (layer.id === 'body' || layer.id === 'head') {
10 | const asset = fs.readFileSync(`./assets/shapes/${layer.id}.svg`, 'utf8')
11 | svg += asset
12 | }
13 | // user layers
14 | if (params) {
15 | const path = `./assets/shapes/${params[layer.id]}.svg`
16 | if (fs.existsSync(path)) {
17 | const asset = fs.readFileSync(path, 'utf8')
18 | svg += asset
19 | }
20 | }
21 | })
22 |
23 | return `
24 | `
27 | }
28 |
29 | module.exports = { build }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pimpmyduck",
3 | "version": "1.0.0",
4 | "description": "Customize your own Zenikanard by Zenika",
5 | "author": "Benjamin Petetot",
6 | "license": "Apache-2.0",
7 | "homepage": "https://pimpmyduck.zenika.com",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/zenika-open-source/my-zenikanard.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/zenika-open-source/my-zenikanard/issues"
14 | },
15 | "private": true,
16 | "workspaces": [
17 | "packages/*"
18 | ],
19 | "scripts": {
20 | "start": "yarn workspace @pimpmyduck/app start",
21 | "build": "yarn workspace @pimpmyduck/app build",
22 | "test": "yarn test:functions",
23 | "test:functions": "yarn workspace @pimpmyduck/functions test"
24 | },
25 | "engines": {
26 | "yarn": ">=1.22.0",
27 | "node": ">=22.0.0"
28 | }
29 | }
--------------------------------------------------------------------------------
/packages/assets/src/shapes/body-tatoo-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/app/src/icons/random.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/body-tatoo-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/head.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/head.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/hat-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/hat-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/app/src/components/Notification.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from 'react'
2 | import cn from 'classnames'
3 |
4 | import styles from './Notification.module.css'
5 |
6 | type NotificationProps = {
7 | actionLabel?: string
8 | onActionClick?: () => void
9 | }
10 |
11 | const Notification: FC = ({
12 | children,
13 | actionLabel = 'OK',
14 | onActionClick,
15 | }) => {
16 | const [open, setOpen] = useState(true)
17 |
18 | const handleAction = () => {
19 | if (onActionClick) onActionClick()
20 | setOpen(false)
21 | }
22 |
23 | if (!open) return null
24 |
25 | return (
26 |
27 |
{children}
28 |
36 |
37 | )
38 | }
39 |
40 | export default Notification
41 |
--------------------------------------------------------------------------------
/packages/app/src/components/AssetButton.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | border: 3px solid gainsboro;
3 | background: gainsboro;
4 | border-radius: 5px;
5 | font-size: 1.4rem;
6 | font-weight: 400;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | width: 70px;
11 | height: 70px;
12 | cursor: pointer;
13 | transition: background-color 100ms ease;
14 | }
15 |
16 | .button:hover:not(.selected) {
17 | background-color: rgb(210, 210, 210);
18 | transition: none;
19 | }
20 |
21 | .button:focus {
22 | outline: none;
23 | }
24 |
25 | .button:active:not(.selected) {
26 | background-color: rgb(190, 190, 190);
27 | }
28 |
29 | .button > svg {
30 | width: 60px;
31 | height: 60px;
32 | }
33 |
34 | .icon {
35 | display: block;
36 | }
37 |
38 | .selected {
39 | border: 3px solid #b51432;
40 | }
41 |
42 | @media (min-width: 800px) {
43 | .button {
44 | margin-bottom: 10px;
45 | }
46 | }
47 |
48 | @media (max-width: 800px) {
49 | .button {
50 | font-size: 1.5rem;
51 | margin-right: 10px;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/app/src/duck.ts:
--------------------------------------------------------------------------------
1 | import { assetNames } from './assets'
2 | import layersData from './assets/layers.json'
3 |
4 | export type SelectedAssets = {
5 | [key: string]: string | undefined
6 | }
7 |
8 | export type Layer = {
9 | id: string
10 | name?: string
11 | categoryOrder?: number
12 | }
13 |
14 | export const DEFAULT_ASSETS: SelectedAssets = {
15 | 'body-tatoo': 'body-tatoo-1',
16 | eye: 'eye-1',
17 | mouth: 'mouth-1',
18 | hair: 'hair-1',
19 | }
20 |
21 | export const getCategoryLayers = () => {
22 | return layers
23 | .filter((layer) => !!layer.name)
24 | .sort((a, b) => {
25 | return (a?.categoryOrder ?? 0) - (b?.categoryOrder ?? 0)
26 | })
27 | }
28 |
29 | export const getDefaultLayer = () => {
30 | return getCategoryLayers()[0]
31 | }
32 |
33 | export const getLayerAssets = (layerId: string) => {
34 | const layerAssets: string[] = []
35 | assetNames.forEach((name) => {
36 | if (name.startsWith(layerId)) {
37 | layerAssets.push(name)
38 | }
39 | })
40 | return layerAssets
41 | }
42 |
43 | export const layers = layersData as Layer[]
44 |
--------------------------------------------------------------------------------
/packages/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@pimpmyduck/app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "license": "Apache-2.0",
6 | "scripts": {
7 | "start": "react-scripts start",
8 | "build": "react-scripts build"
9 | },
10 | "dependencies": {
11 | "classnames": "^2.2.6",
12 | "pwacompat": "^2.0.17",
13 | "query-string": "^6.13.6",
14 | "react": "^17.0.1",
15 | "react-dom": "^17.0.1",
16 | "save-svg-as-png": "^1.4.17"
17 | },
18 | "devDependencies": {
19 | "@types/classnames": "^2.2.10",
20 | "@types/jest": "^26.0.15",
21 | "@types/node": "^14.14.2",
22 | "@types/react": "^16.9.53",
23 | "@types/react-dom": "^16.9.8",
24 | "react-scripts": "^5.0.1",
25 | "typescript": "^4.0.3"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">0.2%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/app"
5 | target-branch: "master"
6 | schedule:
7 | interval: "monthly"
8 | labels:
9 | - "dependencies"
10 | - "automated pr"
11 | groups:
12 | app-packages:
13 | patterns:
14 | - "*"
15 | update-types:
16 | - "minor"
17 | - "patch"
18 | - package-ecosystem: "npm"
19 | directory: "/assets"
20 | target-branch: "master"
21 | schedule:
22 | interval: "monthly"
23 | labels:
24 | - "dependencies"
25 | - "automated pr"
26 | groups:
27 | assets-packages:
28 | patterns:
29 | - "*"
30 | update-types:
31 | - "minor"
32 | - "patch"
33 | - package-ecosystem: "npm"
34 | directory: "/functions"
35 | target-branch: "master"
36 | schedule:
37 | interval: "monthly"
38 | labels:
39 | - "dependencies"
40 | - "automated pr"
41 | groups:
42 | functions-packages:
43 | patterns:
44 | - "*"
45 | update-types:
46 | - "minor"
47 | - "patch"
--------------------------------------------------------------------------------
/packages/app/src/components/AssetButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, Suspense } from 'react'
2 | import cn from 'classnames'
3 | import { Layer } from '../duck'
4 | import { getIcon } from '../assets'
5 |
6 | import { ReactComponent as Ban } from '../icons/ban.svg'
7 | import styles from './AssetButton.module.css'
8 |
9 | type AssetButtonProps = {
10 | assetName?: string
11 | layer: Layer
12 | onClick: (layer: Layer, assetName: string | undefined) => void
13 | selected: boolean
14 | }
15 |
16 | const AssetButton: FC = ({
17 | layer,
18 | assetName,
19 | onClick,
20 | selected,
21 | }) => {
22 | const Icon = getIcon(assetName)
23 | return (
24 |
37 | )
38 | }
39 |
40 | export default AssetButton
41 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/eye-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/eye-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/eye-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/eye-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/app/src/components/Notification.module.css:
--------------------------------------------------------------------------------
1 | .notification {
2 | background: #fff;
3 | max-width: 500px;
4 | min-width: 300px;
5 | padding: 1rem;
6 | color: #333333;
7 | border-radius: 5px;
8 | box-shadow: 0 0 14px rgba(0, 0, 0, 0.24);
9 | position: absolute;
10 | bottom: 1rem;
11 | right: 1rem;
12 | transition: 0.3s;
13 | display: flex;
14 | align-items: center;
15 | }
16 |
17 | @media (max-width: 800px) {
18 | .notification {
19 | width: 100%;
20 | right: 0;
21 | bottom: 110px;
22 | border-radius: 0px;
23 | flex-wrap: nowrap;
24 | flex-direction: row;
25 | }
26 |
27 | .notification > p {
28 | flex-grow: 1;
29 | }
30 | }
31 |
32 | .button {
33 | background: transparent;
34 | padding: 0.4rem 1rem;
35 | border: none;
36 | border-radius: 2px;
37 | outline: none;
38 | text-transform: uppercase;
39 | font-size: 0.8rem;
40 | white-space: nowrap;
41 | cursor: pointer;
42 | transition: 0.2s;
43 | }
44 |
45 | .primary {
46 | background: #b51432;
47 | color: #fff;
48 | box-shadow: 0 0 2px rgba(#b51432, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
49 | margin-left: 1rem;
50 | }
51 |
52 | .primary:hover {
53 | background: #b51432;
54 | box-shadow: 0 0 4px rgba(#b51432, 0.14), 0 4px 8px rgba(0, 0, 0, 0.28);
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/zenika-open-source/my-zenikanard/blob/master/LICENSE) [](https://app.netlify.com/sites/zenikanard/deploys)
4 |
5 | ## Official app
6 |
7 | Play on **[https://pimpmyduck.zenika.com](https://pimpmyduck.zenika.com)**.
8 |
9 | ## Contributing
10 |
11 | Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/zenika-open-source/my-zenikanard/issues).
12 |
13 | See the [Asset proposal documentation](./proposals/README.md), if you want to add a new asset to Pimp My Duck.
14 |
15 | All development instructions are explained in the [contributing guide](./.github/CONTRIBUTING.md).
16 |
17 | ⚠️ For maintainers and changes on icons and shapes, see [this README](packages/assets/README.md).
18 |
19 | ## License
20 |
21 | This project is [Apache-2.0](https://github.com/zenika-open-source/my-zenikanard/blob/master/LICENSE) licensed.
22 | Copyright © 2020 [Zenika](https://oss.zenika.com).
23 | 
24 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/mouth-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/hat-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/hat-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/mouth-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/hat-6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/hat-6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/icons/glasses-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets
2 |
3 | Assets files are copied to `app` and `functions` packages by the npm `postinstall` hook.
4 |
5 | ## Commands
6 |
7 | | Command | Description |
8 | | -------------- | ----------------------------------------------------------------------------------------------- |
9 | | optimize | Optimize all SVG file in `icons` and `shapes` folder with [`svgo`](https://github.com/svg/svgo) |
10 | | generate | Generate a javascript file exporting of all SVG files as React components (used in webapp) |
11 | | copy:app | Copy all assets and generated js files in the `app` package |
12 | | copy:functions | Copy all assets and generated js files in the `functions` package |
13 | | add:asset | Must be executed when new assets are added in `shapes` and `icons` folders |
14 |
15 | ## Package strucure
16 |
17 | ```
18 | packages/assets
19 | ├── design # Contains all designs as `Affinity Designer` format
20 | ├── generate.js # Node script used to generate `src/index.js`
21 | └── src
22 | ├── icons # Contains all icons in svg format
23 | ├── shapes # Contains all shapes in svg format
24 | ├── index.js # Generated file exporting of all SVG files as React components
25 | └── layers.json # Order and name of the different `Duck` layers
26 | ```
27 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/glasses-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/assets/src/shapes/eye-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/functions/svg/svgBuilder.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const svgBuilder = require('./svgBuilder')
3 |
4 | jest.mock('fs')
5 |
6 | describe('listFilesInDirectorySync', () => {
7 | beforeAll(() => {
8 | fs.__mockFiles = {
9 | './assets/shapes/body.svg': '',
10 | './assets/shapes/head.svg': '',
11 | './assets/shapes/eye-1.svg': '',
12 | './assets/shapes/body-tatoo-1.svg': '',
13 | }
14 | })
15 |
16 | it('generate the minimal svg with body and head if no params given', () => {
17 | const svg = svgBuilder.build()
18 | expect(svg).toBe(`
19 |