loop / src / components / AppPreview.jsx
259 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const AppPreview = ({ type = 'phone', src, srcs, crossfadeInterval = 3000, alt = 'App screenshot' }) => {
  const aspectClass = type === 'wide'
    ? 'aspect-[16/10]'
    : 'aspect-[9/19.5]';

  const maxWidthClass = type === 'wide'
    ? 'max-w-md'
    : 'max-w-[240px]';

  const isVideo = src && (src.endsWith('.mp4') || src.endsWith('.webm') || src.endsWith('.m4v'));

  if (srcs && srcs.length > 0) {
    return (
      <div className={`app-preview ${maxWidthClass} ${aspectClass}`}>
        <CrossfadeImages srcs={srcs} interval={crossfadeInterval} alt={alt} />
      </div>
    );
  }

  if (src) {
    return (
      <div className={`app-preview ${maxWidthClass} ${aspectClass}`}>
        {isVideo ? (
          <video autoPlay loop muted playsInline className="w-full h-full object-cover">
            <source src={src} type={src.endsWith('.webm') ? 'video/webm' : 'video/mp4'} />
          </video>
        ) : (
          <img src={src} alt={alt} className="w-full h-full object-cover" />
        )}
      </div>
    );
  }

  return (
    <div className={`app-preview app-preview-placeholder ${maxWidthClass} ${aspectClass}`}>
      <div className="flex flex-col items-center justify-center h-full gap-2">
        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" className="text-white/15">
          <rect x="5" y="2" width="14" height="20" rx="3" />
          <line x1="5" y1="18" x2="19" y2="18" />
        </svg>
        <span className="text-[10px] font-courier text-white/15 tracking-wider uppercase">{alt}</span>
      </div>
    </div>
  );
};

const CrossfadeImages = ({ srcs, interval, alt }) => {
  const [index, setIndex] = useState(0);

  useEffect(() => {
    if (srcs.length <= 1) return;
    const timer = setInterval(() => {
      setIndex((prev) => (prev + 1) % srcs.length);
    }, interval);
    return () => clearInterval(timer);
  }, [srcs.length, interval]);

  return (
    <AnimatePresence mode="popLayout">
      <motion.img
        key={srcs[index]}
        src={srcs[index]}
        alt={alt}
        className="absolute inset-0 w-full h-full object-cover"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        transition={{ duration: 1.2, ease: 'easeInOut' }}
      />
    </AnimatePresence>
  );
};

export default AppPreview;

static mirror of HEAD · about · clone: git clone https://git.makefuture.ro/loop.git