PHP & React PHP & Vue Node & Vue Node & React

senior full-stack developer

Píšu čistý a bezpečný kód, který udrží zdraví Vašeho projektu.

Jsem on-line:
17:00 - 09:00 09:00 - 01:00
Jiří Zapletal - PHP & Vue developer

Hledáte full-stack (backend & frontend) vývojáře? Neváhejte se na mě obrátit.

O mně

Jsem programátor a vývojář webových aplikací všech druhů. Mám přes 17 let zkušeností z oblasti vývoje SW. Mohu být užitečným členem Vašeho vývojářského týmu, ale klidně mi můžete svěřit i kompletní návrh a realizaci.

Vyvíjím aplikace pro firmy a instituce jako je GoPay, Jihočeská univerzita (Bobřík Informatiky), Střechy Bohemia, Osam Trade a další. Pracuji i pro jednotlivce a drobné podnikatele.

Na backendu nejraději používám PHP 8.2, PHPStan lvl 8, Doctrine ORM, Symfony, Nette i Laravel komponenty a když je potřeba, tak sáhnu po kompletním frameworku nebo vlastním nástroji.

Na frontendu preferuji TypeScript, Vite, Vue 3 / composition API, Vanilla JS, občas i React, SCSS a prostě vše, co k tomu patří.

Na GitHubu vlastním přes 70 repozitářů (veřejných a privátních) a jsem contributorem u několika open-source projektů.

Vyvinul jsem si i vlastní nástroje a frameworky pro rychlejší prototypování, realizace a dlouhodobou udržitelnost webových aplikací.

Jednám na rovinu, držím slovo a snažím se maximálně vycházet vstříc - stojím pouze o férovou spolupráci a proto vyžaduji stejný přístup i z druhé strany.

Můj příběh
Jiří Zapletal - Web developer

Technologie a nástroje, které ovládám

PHP
PHP 8.2 Nette Symfony Laravel PHP Stan lvl 8 Doctrine ORM Guzzle Nette Tester
Node.js
Node 18 Strapi Crawlee Apify SDK Playwright Puppeteer Cheerio
Frontend
Vite TypeScript Vanilla JS Vue Svelte React Bootstrap 5 Tailwind SCSS
DB & Storage
Postgres MySQL MariaDB AWS Aurora RabbitMQ Redis
API
JWT OAuth REST GraphQL CURL Postman
Dev-Ops
Docker Shell Nginx Apache PHP-FPM
Cloud
Google Cloud AWS Digital Ocean AppRunner S3 ECR RDS SES Easypanel Apify platform
GIT
GitHub GitLab BitBucket
UX / UI
Figma Affinity Balsamiq Mockups
Nástroje
PHP Storm Github Copilot JIRA Scrum Slack TogglTrack Google Meet Trello
Experimentálně
Elastic MongoDB Pocketbase.io Google Firebase Golang
Hobby
C# C++ Unity Unreal Engine Game dev VR dev

Jak vypadá můj zdrojový kód

SelectBox.svelte
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';

const dispatch = createEventDispatcher();

export let placeholder: string;
export let fieldId: number;
export let value: string;
export let items: string[];

function onChange(event: InputEvent & { target: HTMLSelectElement }) {
  dispatch('change', { fieldId, value: event.target.value });
}

onMount(() => {
  dispatch('change', { fieldId, value });
});
</script>

<label class="block">
  <div class="text-gray-400 mb-2">{placeholder}</div>
  <select bind:value on:change={onChange} class="select select-bordered select-lg w-full">
    {#each items as item}
       <option>{item}</option>
    {/each}
  </select>
</label>
CollectionDatagrid.vue
<script setup lang="ts">
import { ref, inject } from 'vue'
import { megio } from 'megio-api'
import PageHeading from '@/components/layout/PageHeading.vue'
import Datagrid from '@/components/datagrid/Datagrid.vue'
import type IDatagridSettings from '@/components/datagrid/types/IDatagridSettings'
import type ICollectionSummary from '@/components/collection/types/ICollectionSummary'
import type { IPagination, IRespShow, IRow } from 'megio-api/types/collections'

const props = defineProps<{ tableName: string }>()
const emits = defineEmits<{ (e: 'onLoadingChange', status: boolean): void }>()

const actions: IDatagridSettings['actions'] | undefined = inject('datagrid-actions')
const summaries: ICollectionSummary[] | undefined = inject('collection-summaries')

const loading = ref<boolean>(true)
const datagrid = ref()

async function loadFunction(newPagination: IPagination): Promise<IRespShow> {
    loading.value = true
    emits('onLoadingChange', loading.value)

    const resp = await megio.collections.show({
        table: props.tableName,
        schema: true,
        currentPage: newPagination.currentPage,
        itemsPerPage: newPagination.itemsPerPage,
        orderBy: [
            { col: 'createdAt', desc: true },
            { col: 'id', desc: true }
        ]
    })

    loading.value = false
    emits('onLoadingChange', loading.value)

    return resp
}

function handleFirstColumnClick(row: IRow) {
    const custom = summaries?.filter(sum => sum.collectionName === props.tableName).shift()
    if (custom) {
        custom.onFirstColumnClick(props.tableName, row)
    } else {
        console.log('open sideModal by default')
    }
}

</script>

<template>
    <div class="h-100" v-show="!loading">
        <PageHeading
            v-if="!loading"
            :breadcrumb="['Kolekce', tableName]"
            @onRefresh="() => datagrid.refresh()"
        />
        <Datagrid
            v-if="actions"
            ref="datagrid"
            class="mt-5"
            :key="tableName"
            :loadFunction="loadFunction"
            :rowActions="actions.row"
            :bulkActions="actions.bulk"
            :allowActionsFiltering="true"
            :defaultItemsPerPage="15"
            :loading="loading"
            emptyDataMessage="Data nejsou k dispozici."
            @onFirstColumnClick="handleFirstColumnClick"
        />
    </div>
</template>
PersonalInfo.tsx
import * as yup from 'yup';
import { useAtom } from 'jotai';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { personalDataStore, stepStore } from '../store/store';
import Header from '../components/section/Header';
import NavButtons from '../components/section/NavButtons';
import TextInput from '../components/forms/inputs/TextInput';

type FormValues = {
  name: string;
  email: string;
  phone: string;
};

const schema = yup.object({
  name: yup.string().required('Name is required'),
  email: yup
    .string()
    .required('Email is required')
    .test((value, context) => {
      return /^\S+@\S+\.\S+$/.test(value) || context.createError({ message: 'E-mail is not valid' });
    }),
  phone: yup
    .string()
    .required('Phone is required')
    .test((value, context) => {
      return /^(\+?420)?(\d?){9}$/.test(value) || context.createError({ message: 'Phone is not valid' });
    })
    .transform((value) => (value ? value.replace(/\s/g, '') : value)),
});

function PersonalInfo(): JSX.Element {
  const [, setStep] = useAtom(stepStore);
  const [pData, setPersonalData] = useAtom(personalDataStore);

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({ resolver: yupResolver(schema), values: pData });

  const onSubmit = handleSubmit((data) => {
    setPersonalData(data);
    setStep(2);
  });

  return (
    <form onSubmit={onSubmit} className="grid grid-cols-1 place-content-between h-full">
      <Header
        title="Personal info"
        description="Please provide your name, email address, and phone number."
      />
      <div className="grid gap-5 mt-5 xl:mt-0">
        <TextInput label="Name" fieldName="name" error={errors.name} register={register} />
        <TextInput label="E-mail Address" fieldName="email" error={errors.email} register={register} />
        <TextInput
          label="Phone Number"
          fieldName="phone"
          placeholder="e.g. +420 123 456 789"
          error={errors.phone}
          register={register}
        />
      </div>
      <NavButtons btnText="Next step" />
    </form>
  );
}

export default PersonalInfo;
Resource\UpdateRoleRequest.php
<?php
declare(strict_types=1);

namespace Megio\Http\Request\Resource;

use Nette\Schema\Expect;
use Megio\Database\Entity\Auth\Resource;
use Megio\Database\Entity\Auth\Role;
use Megio\Database\EntityManager;
use Megio\Http\Request\Request;
use Symfony\Component\HttpFoundation\Response;

class UpdateRoleRequest extends Request
{
    public function __construct(protected EntityManager $em)
    {
    }

    public function schema(): array
    {
        return [
            'resource_id' => Expect::string()->required(),
            'role_id' => Expect::string()->required(),
            'enable' => Expect::bool()->required(),
        ];
    }

    public function process(array $data): Response
    {
        $resource = $this->em->getAuthResourceRepo()->findOneBy(['id' => $data['resource_id']]);
        $role = $this->em->getAuthRoleRepo()->findOneBy(['id' => $data['role_id']]);

        if (!$role || !$resource) {
            return $this->error(['Role or resource not found'], 404);
        }

        if ($data['enable'] === true) {
            $role->addResource($resource);
        } else {
            $role->getResources()->removeElement($resource);
        }

        $this->em->flush($role);

        return $this->json(['message' => 'Resources successfully updated']);
    }
}
firebase/scraper.ts
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { CallableContext } from 'firebase-functions/lib/providers/https'
import { ComparatorResult } from './types/ComparatorResult'
import { AllowedTypes } from './types/ScraperType'
import Puppeteer from './scraper/puppeteer'

admin.initializeApp()

export const runPuppeteer = functions
.region('europe-west3')
.runWith({
    timeoutSeconds: 120,
    memory: '4GB',
    failurePolicy: false
})
.https.onCall(async (data: any, context: CallableContext): Promise<Array<ComparatorResult>> => {
    const comparatorResults: Array<ComparatorResult> = []
    const links = data.links.filter((item: string, index: number, self: string)
        => self.indexOf(item) === index).splice(0, 10)

    if (! AllowedTypes.includes(data.scraperType)) {
        comparatorResults.push({
            error: `Scraper typu '${data.scraperType} neexistuje.'`,
            url: null,
            duration: null,
            listingResult: null,
            debugLink: null
        })
        return comparatorResults
    }

    const scraper = new Puppeteer(data.scraperType, true, null, 1500)
    await scraper.createBrowser(1920, 1080)

    for (const targetUrl of links) {
        try {
            const comparatorResult = await scraper.getPageResult(targetUrl)
            comparatorResults.push(comparatorResult)
        } catch (exception) {
            comparatorResults.push({
                error: exception.message,
                url: targetUrl,
                duration: null,
                listingResult: null,
                debugLink: null
            })
        }
    }

    await scraper.closeBrowser()

    return comparatorResults
})
Dockerfile
FROM node:18-alpine as build-stage-node
WORKDIR /build

COPY . ./

RUN yarn cache clean --mirror
RUN yarn && yarn build

FROM php:8.2-fpm-alpine
WORKDIR /var/www/html

# Set timezone
ENV TZ="Europe/Prague"

# Nginx & PHP configs
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/nginx/http.d/default.conf /etc/nginx/http.d/default.conf
COPY ./docker/php/php.ini /usr/local/etc/php/conf.d/php.ini
#COPY ./docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini

# Install core linux dependencies
RUN apk add openssl curl ca-certificates
RUN apk add bash nano
RUN apk add nginx

# Supported dependencies
# https://github.com/mlocati/docker-php-extension-installer#supported-php-extensions

# Install opcache
RUN docker-php-ext-install opcache

# Install intl
RUN apk add --no-cache icu-dev
RUN docker-php-ext-configure intl
RUN docker-php-ext-install intl

# Install postgres
#RUN apk add --no-cache libpq-dev
#RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql
#RUN docker-php-ext-install pdo pdo_pgsql

# Intall GD
#RUN apk add --no-cache freetype libpng libjpeg-turbo freetype-dev libpng-dev libjpeg-turbo-dev
#RUN docker-php-ext-configure gd --with-freetype --with-jpeg
#RUN NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1)
#RUN docker-php-ext-install -j$(nproc) gd
#RUN apk del --no-cache freetype-dev libpng-dev libjpeg-turbo-dev

# Install zip
#RUN apk add zip libzip-dev #git libicu-dev curl gnupg
#RUN docker-php-ext-configure zip
#RUN docker-php-ext-install zip

# Install PCNTL
#RUN docker-php-ext-configure pcntl --enable-pcntl
#RUN docker-php-ext-install pcntl

# Copy source code
COPY . ./
COPY --from=build-stage-node /build/www/temp ./www/temp
#COPY --from=build-stage-node /build/temp/latte-mail ./temp/latte-mail

# Install composer & dependencies
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN composer install --no-cache --prefer-dist --no-scripts

# Resolve permissions
RUN chmod -R ugo+w ./temp
RUN chmod -R ugo+w ./log
RUN chmod -R ugo+r ./www/temp
RUN chown -R www-data:www-data /var/www/html

# Add entrypoint
ADD ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]
side-modal.scss
.side-modal {
    top: 0;
    left: 0;
    right: 0;
    z-index: 1005;
    background: rgba(var(--v-theme-on-surface), .32);
    transition: 300ms ease opacity;
    opacity: 0;
    pointer-events: none;

    &.opened {
        opacity: 1;
        pointer-events: auto;

        .side-modal-container {
            transform: translateX(0);
        }
    }

    &-container {
        max-width: 700px;
        width: 100%;
        z-index: 2;
        right: 0;
        transition: 300ms ease transform;
        transform: translateX(100%);
    }

    &-close {
        left: -55px;
        top: 20px
    }
}

Reference

Slova přímo od zákazníků (můžete jim zavolat a cokoli si ověřit).

Zkušenosti prověřené desítkami klientů a stovkami jejich projektů

Jednoduše se pobavme o spolupráci

1

Přes webový formulář mi na sebe zanecháte kontakt a případně uvedete podrobnosti.

2

Telefonicky se s Vámi spojím a naplánujeme termín krátké on-line schůzky.

3

Lidsky se pobavíme o představách, vzájemných možnostech a formě spolupráce.

Ušetříte si nervy, čas i peníze

Vyvíjím totiž nástroje, které ve vteřině zastanou běžnou i složitější práci několika programátorů.

Kontakt

Zdarma se pobavíme o projektu, představách a vzájemných možnostech, které povedou ke kvalitní spolupráci.

Telefon
+420 606 091 125
E-mail
jz@strategio.dev
Kde mě naleznete

Pracuji na Full-Remote z Calgary (Kanada).

Sejít se můžeme v kavárně, on-line, ve VR nebo u Vás na firmě.

Fakturační údaje
Jiří Zapletal
Luční 243
Nová Bystřice 378 33
IČO: 01367137
DIČ: neplátce DPH
2301859766/2010
Strategio Digital s.r.o.
Radniční 133/1
Č. Budějovice 370 01
IČO: 09568042
DIČ: neplátce DPH
2801878605/2010

Dostávejte to nejnovější z oblasti vývoje aplikací rovnou na Váš e-mail.

Máte dotazy?

ZDARMA Vám poskytnu konzultaci, poradím a navrhnu řešení.

Zanechte mi vzkaz nebo se mi rovnou ozvěte na některý z uvedených kontaktů.

Jiří Zapletal

Senior PHP developer na volné noze. 17 let zkušeností v oblasti vývoje. Stovky realizovaných projektů.

Telefon: +420 606 091 125
© 2024 | copy design dev & mkt by Jiří Zapletal