CoolFace
Apppublic

DataScope26/WEB

sourceHugging Faceupdated 7d agoView on Hugging Face
0likes
StepCompanyData.jsx531 linesDownload Raw Back to components
1import React, { useState, useRef, useEffect } from 'react';2import { ArrowLeft, Loader } from 'lucide-react';3 4import './StepCompanyData.css';5import ButtonPrimary from './ButtonPrimary.jsx';6import ButtonSecondary from './ButtonSecondary.jsx';7import ButtonAlert from './ButtonAlert.jsx';8 9const CROP_SIZE = 280;10const OUTPUT_SIZE = 500;11 12export default function StepCompanyData({ data, updateData, onBack, onSubmit, isSubmitting, errors = {} }) {13  const isFormValid = data.name && data.legalName;14  const [validationErrors, setValidationErrors] = useState({});15  const [showLogoModal, setShowLogoModal] = useState(false);16  const [selectedLogoUrl, setSelectedLogoUrl] = useState(null);17  const [logoScale, setLogoScale] = useState(1);18  const [logoMinScale, setLogoMinScale] = useState(1);19  const [logoOffset, setLogoOffset] = useState({ x: 0, y: 0 });20  const [isSavingLogo, setIsSavingLogo] = useState(false);21 22  const fileInputRef = useRef(null);23  const logoCanvasRef = useRef(null);24  const logoImgRef = useRef(null);25  const isDraggingRef = useRef(false);26  const lastPosRef = useRef({ x: 0, y: 0 });27 28  const companyLogo = data.logo && data.logo.trim() ? data.logo : '';29 30  const openLogoModal = () => {31    setSelectedLogoUrl(null);32    setLogoScale(1);33    setLogoMinScale(1);34    setLogoOffset({ x: 0, y: 0 });35    setShowLogoModal(true);36  };37 38  const closeLogoModal = () => {39    if (isSavingLogo) return;40 41    if (selectedLogoUrl) {42      URL.revokeObjectURL(selectedLogoUrl);43    }44 45    setSelectedLogoUrl(null);46    setShowLogoModal(false);47  };48 49  const handleLogoChosen = (e) => {50    const file = e.target.files?.[0];51    if (!file) return;52 53    if (!file.type.startsWith('image/')) {54      return;55    }56 57    const url = URL.createObjectURL(file);58 59    if (selectedLogoUrl) {60      URL.revokeObjectURL(selectedLogoUrl);61    }62 63    setSelectedLogoUrl(url);64    setLogoScale(1);65    setLogoMinScale(1);66    setLogoOffset({ x: 0, y: 0 });67    e.target.value = '';68  };69 70  const triggerLogoFileSelect = () => {71    fileInputRef.current?.click();72  };73 74  useEffect(() => {75    if (!selectedLogoUrl) {76      logoImgRef.current = null;77      return;78    }79 80    const img = new Image();81 82    img.onload = () => {83      logoImgRef.current = img;84 85      const coverScale = Math.max(86        CROP_SIZE / img.width,87        CROP_SIZE / img.height88      );89 90      setLogoMinScale(coverScale);91      setLogoScale(coverScale);92 93      const drawWidth = img.width * coverScale;94      const drawHeight = img.height * coverScale;95 96      setLogoOffset({97        x: (CROP_SIZE - drawWidth) / 2,98        y: (CROP_SIZE - drawHeight) / 2,99      });100    };101 102    img.src = selectedLogoUrl;103  }, [selectedLogoUrl]);104 105  const clampOffset = (nextOffset, currentScale) => {106    const img = logoImgRef.current;107 108    if (!img) return nextOffset;109 110    const drawWidth = img.width * currentScale;111    const drawHeight = img.height * currentScale;112 113    const minX = CROP_SIZE - drawWidth;114    const minY = CROP_SIZE - drawHeight;115 116    return {117      x: Math.min(0, Math.max(minX, nextOffset.x)),118      y: Math.min(0, Math.max(minY, nextOffset.y)),119    };120  };121 122  useEffect(() => {123    const canvas = logoCanvasRef.current;124    const img = logoImgRef.current;125 126    if (!canvas || !img || !selectedLogoUrl) return;127 128    const ctx = canvas.getContext('2d');129    ctx.clearRect(0, 0, CROP_SIZE, CROP_SIZE);130    ctx.drawImage(131      img,132      logoOffset.x,133      logoOffset.y,134      img.width * logoScale,135      img.height * logoScale136    );137  }, [logoScale, logoOffset, selectedLogoUrl]);138 139  const handleLogoZoomChange = (e) => {140    const newScale = Number.parseFloat(e.target.value);141    const center = CROP_SIZE / 2;142 143    // Punto de la imagen que actualmente está en el centro del círculo de recorte.144    const imgCenterX = (center - logoOffset.x) / logoScale;145    const imgCenterY = (center - logoOffset.y) / logoScale;146 147    // Recalculamos el offset para que ese mismo punto siga en el centro148    // después de aplicar el nuevo scale (zoom centrado, no desde la esquina).149    const nextOffset = {150      x: center - imgCenterX * newScale,151      y: center - imgCenterY * newScale,152    };153 154    setLogoScale(newScale);155    setLogoOffset(clampOffset(nextOffset, newScale));156  };157 158  const handleLogoPointerDown = (e) => {159    if (!logoImgRef.current) return;160 161    isDraggingRef.current = true;162    const point = e.touches ? e.touches[0] : e;163 164    lastPosRef.current = {165      x: point.clientX,166      y: point.clientY,167    };168  };169 170  const handleLogoPointerMove = (e) => {171    if (!isDraggingRef.current || !logoImgRef.current) return;172 173    const point = e.touches ? e.touches[0] : e;174    const dx = point.clientX - lastPosRef.current.x;175    const dy = point.clientY - lastPosRef.current.y;176 177    lastPosRef.current = {178      x: point.clientX,179      y: point.clientY,180    };181 182    setLogoOffset((prev) =>183      clampOffset(184        {185          x: prev.x + dx,186          y: prev.y + dy,187        },188        logoScale189      )190    );191  };192 193  const handleLogoPointerUp = () => {194    isDraggingRef.current = false;195  };196 197  const handleSaveCroppedLogo = async () => {198    const img = logoImgRef.current;199    if (!img) return;200 201    setIsSavingLogo(true);202 203    try {204      const outputCanvas = document.createElement('canvas');205      outputCanvas.width = OUTPUT_SIZE;206      outputCanvas.height = OUTPUT_SIZE;207 208      const ctx = outputCanvas.getContext('2d');209      ctx.imageSmoothingEnabled = true;210      ctx.imageSmoothingQuality = 'high';211 212      const ratio = OUTPUT_SIZE / CROP_SIZE;213 214      ctx.drawImage(215        img,216        logoOffset.x * ratio,217        logoOffset.y * ratio,218        img.width * logoScale * ratio,219        img.height * logoScale * ratio220      );221 222      const webpBlob = await new Promise((resolve, reject) => {223        outputCanvas.toBlob(224          (blob) => {225            if (!blob) {226              reject(new Error('Failed to create image'));227              return;228            }229            resolve(blob);230          },231          'image/webp',232          0.9233        );234      });235 236      const croppedFile = new File([webpBlob], 'company-logo.webp', {237        type: 'image/webp',238      });239 240      const previewUrl = URL.createObjectURL(webpBlob);241      updateData({242        logo: previewUrl,243        logoFile: croppedFile,244      });245 246      if (selectedLogoUrl) {247        URL.revokeObjectURL(selectedLogoUrl);248      }249 250      setSelectedLogoUrl(null);251      setShowLogoModal(false);252    } catch (error) {253      console.error('Error al guardar el logo de la empresa:', error);254    } finally {255      setIsSavingLogo(false);256    }257  };258 259  const handleRemoveLogo = () => {260    if (companyLogo) {261      URL.revokeObjectURL(companyLogo);262    }263 264    updateData({265      logo: '',266      logoFile: null,267    });268  };269 270  // Validation rules271  const allowedRe = /^[\p{L}\d .-]+$/u; // Unicode letters, digits, space, dot, hyphen272 273  const validateName = (value) => {274    if (!value || String(value).trim() === '') {275      return 'El nombre es obligatorio.';276    }277 278    if (!/[A-Za-z]/.test(String(value))) {279      return 'El nombre debe contener al menos una letra (A–Z).';280    }281 282    if (!allowedRe.test(String(value))) {283      return 'El nombre solo puede contener letras, números, espacios, puntos y guiones.';284    }285 286    return null;287  };288 289  const validateLegalName = (value) => {290    if (!value || String(value).trim() === '') {291      return 'La razón social es obligatoria.';292    }293    // must include at least one latin letter as well294    if (!/[A-Za-z]/.test(String(value))) {295      return 'La razón social debe contener al menos una letra (A–Z).';296    }297 298    if (!allowedRe.test(String(value))) {299      return 'La razón social solo puede contener letras, números, espacios, puntos y guiones.';300    }301 302    return null;303  };304 305  const handleNameChange = (val) => {306    updateData({ name: val });307    setValidationErrors((prev) => {308      const next = { ...prev };309      const err = validateName(val);310      if (err) next.name = err; else delete next.name;311      return next;312    });313  };314 315  const handleLegalNameChange = (val) => {316    updateData({ legalName: val });317    setValidationErrors((prev) => {318      const next = { ...prev };319      const err = validateLegalName(val);320      if (err) next.legalName = err; else delete next.legalName;321      return next;322    });323  };324 325  const handleNext = () => {326    const nameErr = validateName(data.name);327    const legalErr = validateLegalName(data.legalName);328 329    const nextErrors = {};330    if (nameErr) nextErrors.name = nameErr;331    if (legalErr) nextErrors.legalName = legalErr;332 333    setValidationErrors(nextErrors);334 335    if (Object.keys(nextErrors).length > 0) return;336 337    if (onSubmit) onSubmit();338  };339 340  return (341    <div className="company-data-content">342      <div className="company-data-header">343        <div className="company-data-subtitle-row">344          <ButtonSecondary className="step-back-button" onClick={onBack} aria-label="Volver atrás">345            <ArrowLeft size={20} />346          </ButtonSecondary>347          <div className="company-data-subtitle">Datos generales</div>348        </div>349        <h2 className="company-data-title">Datos de la Organización</h2>350      </div>351 352      <div className="company-data-form">353        <div className="form-input-group">354          <label>Nombre de la Compañía *</label>355          <input356            type="text"357            placeholder="Ej: DataScope Analytics"358            value={data.name}359            onChange={(e) => updateData({ name: e.target.value })}360            style={(errors.name || validationErrors.name) ? { borderColor: '#e53e3e' } : {}}361            maxLength={50}362          />363          {(validationErrors.name || errors.name) && (364            <span className="field-error">{validationErrors.name || errors.name}</span>365          )}366        </div>367 368        <div className="form-input-group">369          <label>Razón Social *</label>370          <input371            type="text"372            placeholder="Ej: DataScope S.A."373            value={data.legalName}374            onChange={(e) => updateData({ legalName: e.target.value })}375            style={errors.legalName ? { borderColor: '#e53e3e' } : {}}376            maxLength={100}377          />378          {errors.legalName && (379            <span className="field-error">{errors.legalName}</span>380          )}381        </div>382 383        <div className="form-input-group company-logo-upload-group">384          <label>LOGO DE LA COMPAÑÍA (OPCIONAL)</label>385 386          <div className="company-logo-picker">387            <button388              type="button"389              className="company-logo-preview"390              onClick={openLogoModal}391              aria-label="Seleccionar logo de la empresa"392            >393              {companyLogo ? (394                <img src={companyLogo} alt="Logo de la empresa" className="company-logo-image" />395              ) : (396                <div className="company-logo-placeholder">+</div>397              )}398            </button>399 400            <div className="company-logo-actions">401              <ButtonSecondary402                type="button"403                className="company-logo-button"404                onClick={openLogoModal}405              >406                {companyLogo ? 'Cambiar logo' : 'Seleccionar logo'}407              </ButtonSecondary>408 409              {companyLogo && (410                <ButtonAlert411                  type="button"412                  className="company-logo-remove"413                  onClick={handleRemoveLogo}414                >415                  Quitar416                </ButtonAlert>417              )}418            </div>419          </div>420 421          {errors.logo && (422            <span className="field-error">{errors.logo}</span>423          )}424        </div>425      </div>426 427      <div className="company-data-actions">428        {isSubmitting ? (429          <ButtonPrimary className="btn-next" style={{ cursor: 'default' }}>430            <Loader size={18} className="loader-icon-animation" />431          </ButtonPrimary>432        ) : (433          <ButtonPrimary434            onClick={handleNext}435            disabled={!isFormValid}436            className="btn-next"437            style={{ opacity: isFormValid ? 1 : 0.5, cursor: isFormValid ? 'pointer' : 'not-allowed' }}438          >439            Continuar440          </ButtonPrimary>441        )}442      </div>443 444      {showLogoModal && (445        <div className="cs-logo-modal-overlay" onClick={closeLogoModal}>446          <div className="cs-logo-modal-content" onClick={(e) => e.stopPropagation()}>447            <h3 className="cs-logo-modal-title">Logo de la empresa</h3>448 449            {!selectedLogoUrl && (450              <>451                <div className="cs-logo-current-frame">452                  {companyLogo ? (453                    <img className="cs-logo-current-image" src={companyLogo} alt="Logo actual de la empresa" />454                  ) : (455                    <div className="cs-logo-current-placeholder">+</div>456                  )}457                </div>458                <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>459                  Cambiar logo460                </ButtonPrimary>461              </>462            )}463 464            {selectedLogoUrl && (465              <>466                <div467                  className="cs-logo-crop-container"468                  onMouseDown={handleLogoPointerDown}469                  onMouseMove={handleLogoPointerMove}470                  onMouseUp={handleLogoPointerUp}471                  onMouseLeave={handleLogoPointerUp}472                  onTouchStart={handleLogoPointerDown}473                  onTouchMove={handleLogoPointerMove}474                  onTouchEnd={handleLogoPointerUp}475                >476                  <canvas ref={logoCanvasRef} width={CROP_SIZE} height={CROP_SIZE} />477                  <div className="cs-logo-crop-grid" />478                </div>479 480                <div className="cs-logo-zoom-controls">481                  <span className="cs-logo-zoom-label">-</span>482                  <input483                    className="cs-logo-zoom-slider"484                    type="range"485                    min={logoMinScale}486                    max={logoMinScale * 4}487                    step={0.01}488                    value={logoScale}489                    onChange={handleLogoZoomChange}490                  />491                  <span className="cs-logo-zoom-label">+</span>492                </div>493 494                <ButtonPrimary type="button" className="btn-change-photo" onClick={triggerLogoFileSelect}>495                  Elegir otro logo496                </ButtonPrimary>497              </>498            )}499 500            <input501              ref={fileInputRef}502              type="file"503              accept="image/*"504              onChange={handleLogoChosen}505              style={{ display: 'none' }}506            />507 508            <div className="cs-logo-modal-actionss">509              <ButtonPrimary510                type="button"511                className="btn-save"512                disabled={!selectedLogoUrl || isSavingLogo}513                onClick={handleSaveCroppedLogo}514              >515                {isSavingLogo ? 'Guardando...' : 'Guardar logo'}516              </ButtonPrimary>517              <ButtonAlert518                type="button"519                className="btn-logout"520                onClick={closeLogoModal}521                disabled={isSavingLogo}522              >523                Cancelar524              </ButtonAlert>525            </div>526          </div>527        </div>528      )}529    </div>530  );531}