Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Grid Layouts i Modals - Instagram UI

Czas stworzyć interfejs galerii inspirowany Instagramem. Nauczysz się tworzyć grid layouts i modale.

Grid Layout - Siatka zdjęć

Tailwind CSS ma świetne klasy dla grid layouts.

Podstawowy grid 3x3:

1<div className="grid grid-cols-3 gap-4">
2  <div className="bg-gray-200 aspect-square">Zdjęcie 1</div>
3  <div className="bg-gray-200 aspect-square">Zdjęcie 2</div>
4  <div className="bg-gray-200 aspect-square">Zdjęcie 3</div>
5  <div className="bg-gray-200 aspect-square">Zdjęcie 4</div>
6  <div className="bg-gray-200 aspect-square">Zdjęcie 5</div>
7  <div className="bg-gray-200 aspect-square">Zdjęcie 6</div>
8</div>

Klasy:

  • grid
    - aktywuje CSS Grid
  • grid-cols-3
    - 3 kolumny
  • gap-4
    - odstęp 16px między elementami
  • aspect-square
    - kwadratowy kształt (1:1)

Responsywny grid:

1<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-1">
2  {/* Zdjęcia */}
3</div>

Zachowanie:

  • Mobile: 1 kolumna
  • Small (≥640px): 2 kolumny
  • Medium (≥768px): 3 kolumny
  • Large (≥1024px): 4 kolumny

Instagram Gallery Grid

1import Image from 'next/image';
2
3interface Photo {
4  id: number;
5  url: string;
6  caption: string;
7  likes: number;
8}
9
10export default function TreasureGallery({ photos }: { photos: Photo[] }) {
11  return (
12    <div className="max-w-5xl mx-auto p-4">
13      <h1 className="text-2xl font-bold mb-6">Treasure Gallery</h1>
14
15      <div className="grid grid-cols-3 gap-1">
16        {photos.map((photo) => (
17          <div
18            key={photo.id}
19            className="relative aspect-square group cursor-pointer overflow-hidden"
20          >
21            <Image
22              src={photo.url}
23              alt={photo.caption}
24              fill
25              className="object-cover transition-transform group-hover:scale-110"
26            />
27
28            {/* Overlay przy hover */}
29            <div className="absolute inset-0 bg-black bg-opacity-60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
30              <div className="text-white text-center space-y-2">
31                <p className="text-2xl font-bold">❤️ {photo.likes}</p>
32                <p className="text-sm px-4">{photo.caption}</p>
33              </div>
34            </div>
35          </div>
36        ))}
37      </div>
38    </div>
39  );
40}

Modal Dialog - Powiększone zdjęcie

Modal to okno które pojawia się na wierzchu strony. Użyjemy go do pokazania pełnego zdjęcia.

Prosty Modal z useState:

1'use client';
2
3import { useState } from 'react';
4import Image from 'next/image';
5
6interface Photo {
7  id: number;
8  url: string;
9  caption: string;
10  author: string;
11  likes: number;
12}
13
14export default function Gallery({ photos }: { photos: Photo[] }) {
15  const [selectedPhoto, setSelectedPhoto] = useState<Photo | null>(null);
16
17  return (
18    <>
19      {/* Grid */}
20      <div className="grid grid-cols-3 gap-1">
21        {photos.map((photo) => (
22          <div
23            key={photo.id}
24            onClick={() => setSelectedPhoto(photo)}
25            className="relative aspect-square cursor-pointer"
26          >
27            <Image src={photo.url} alt={photo.caption} fill className="object-cover" />
28          </div>
29        ))}
30      </div>
31
32      {/* Modal */}
33      {selectedPhoto && (
34        <div
35          className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
36          onClick={() => setSelectedPhoto(null)}
37        >
38          <div
39            className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-hidden"
40            onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
41          >
42            {/* Header */}
43            <div className="flex items-center justify-between p-4 border-b">
44              <div className="flex items-center gap-3">
45                <div className="text-2xl">🏴‍☠️</div>
46                <h3 className="font-bold">{selectedPhoto.author}</h3>
47              </div>
48              <button
49                onClick={() => setSelectedPhoto(null)}
50                className="text-gray-500 hover:text-gray-700 text-2xl"
51              >
5253              </button>
54            </div>
55
56            {/* Image */}
57            <div className="relative aspect-square">
58              <Image
59                src={selectedPhoto.url}
60                alt={selectedPhoto.caption}
61                fill
62                className="object-contain"
63              />
64            </div>
65
66            {/* Footer */}
67            <div className="p-4 border-t">
68              <div className="flex items-center gap-4 mb-2">
69                <button className="text-2xl hover:scale-110 transition-transform">
70                  ❤️
71                </button>
72                <button className="text-2xl hover:scale-110 transition-transform">
73                  💬
74                </button>
75                <button className="text-2xl hover:scale-110 transition-transform">
76                  📤
77                </button>
78              </div>
79              <p className="font-bold mb-1">{selectedPhoto.likes} polubień</p>
80              <p className="text-gray-800">
81                <span className="font-bold">{selectedPhoto.author}</span>{' '}
82                {selectedPhoto.caption}
83              </p>
84            </div>
85          </div>
86        </div>
87      )}
88    </>
89  );
90}

Portal Pattern - Lepsze modale

Dla production używaj bibliotek jak Radix UI lub Headless UI, ale oto prosty portal:

1'use client';
2
3import { useEffect, useState } from 'react';
4import { createPortal } from 'react-dom';
5
6function Modal({
7  isOpen,
8  onClose,
9  children
10}: {
11  isOpen: boolean;
12  onClose: () => void;
13  children: React.ReactNode;
14}) {
15  const [mounted, setMounted] = useState(false);
16
17  useEffect(() => {
18    setMounted(true);
19    return () => setMounted(false);
20  }, []);
21
22  useEffect(() => {
23    if (isOpen) {
24      document.body.style.overflow = 'hidden'; // Blokuj scroll
25    } else {
26      document.body.style.overflow = 'unset';
27    }
28  }, [isOpen]);
29
30  if (!mounted || !isOpen) return null;
31
32  return createPortal(
33    <div
34      className="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"
35      onClick={onClose}
36    >
37      <div onClick={(e) => e.stopPropagation()}>
38        {children}
39      </div>
40    </div>,
41    document.body
42  );
43}
44
45export default Modal;

Keyboard Navigation - Escape to close

Dodaj obsługę klawiatury:

1'use client';
2
3import { useEffect } from 'react';
4
5function useKeyPress(key: string, callback: () => void) {
6  useEffect(() => {
7    const handler = (e: KeyboardEvent) => {
8      if (e.key === key) {
9        callback();
10      }
11    };
12
13    window.addEventListener('keydown', handler);
14    return () => window.removeEventListener('keydown', handler);
15  }, [key, callback]);
16}
17
18function Modal({ isOpen, onClose, children }: ModalProps) {
19  useKeyPress('Escape', onClose);
20
21  // Reszta kodu...
22}

Image Carousel - Przełączanie zdjęć

Dodaj strzałki do przełączania między zdjęciami:

1'use client';
2
3import { useState } from 'react';
4
5export default function Gallery({ photos }: { photos: Photo[] }) {
6  const [currentIndex, setCurrentIndex] = useState(0);
7  const [isModalOpen, setIsModalOpen] = useState(false);
8
9  const nextPhoto = () => {
10    setCurrentIndex((prev) => (prev + 1) % photos.length);
11  };
12
13  const prevPhoto = () => {
14    setCurrentIndex((prev) => (prev - 1 + photos.length) % photos.length);
15  };
16
17  const currentPhoto = photos[currentIndex];
18
19  return (
20    <>
21      {/* Grid - kliknięcie otwiera modal z danym indexem */}
22      <div className="grid grid-cols-3 gap-1">
23        {photos.map((photo, index) => (
24          <div
25            key={photo.id}
26            onClick={() => {
27              setCurrentIndex(index);
28              setIsModalOpen(true);
29            }}
30            className="relative aspect-square cursor-pointer"
31          >
32            <Image src={photo.url} alt={photo.caption} fill className="object-cover" />
33          </div>
34        ))}
35      </div>
36
37      {/* Modal z carousel */}
38      {isModalOpen && (
39        <div className="fixed inset-0 bg-black bg-opacity-95 z-50 flex items-center justify-center">
40          <button
41            onClick={prevPhoto}
42            className="absolute left-4 text-white text-4xl hover:scale-110 transition-transform"
43          >
4445          </button>
46
47          <div className="max-w-4xl w-full">
48            <div className="relative aspect-square">
49              <Image
50                src={currentPhoto.url}
51                alt={currentPhoto.caption}
52                fill
53                className="object-contain"
54              />
55            </div>
56            <p className="text-white text-center mt-4">{currentPhoto.caption}</p>
57          </div>
58
59          <button
60            onClick={nextPhoto}
61            className="absolute right-4 text-white text-4xl hover:scale-110 transition-transform"
62          >
6364          </button>
65
66          <button
67            onClick={() => setIsModalOpen(false)}
68            className="absolute top-4 right-4 text-white text-3xl"
69          >
7071          </button>
72        </div>
73      )}
74    </>
75  );
76}

Masonry Layout - Pinterest style

Dla bardziej zaawansowanego layoutu (różne wysokości):

1<div className="columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-4 space-y-4">
2  {photos.map((photo) => (
3    <div key={photo.id} className="break-inside-avoid">
4      <Image
5        src={photo.url}
6        alt={photo.caption}
7        width={300}
8        height={Math.random() * 200 + 200} // Różne wysokości
9        className="w-full rounded-lg"
10      />
11    </div>
12  ))}
13</div>

Podsumowanie 🎓

Grid Layout -

grid grid-cols-3 gap-4
Responsywny grid -
grid-cols-1 md:grid-cols-3
aspect-square - kwadratowe elementy ✅ Modal - overlay z fixed position ✅ e.stopPropagation() - prevent closing modal on inside click ✅ Keyboard handling - Escape to close ✅ Image carousel - prev/next navigation ✅ Hover effects - group-hover:opacity-100 ✅ Masonry layout - columns-* dla Pinterest style

W następnym ćwiczeniu dodamy funkcję dodawania zdjęć i komentarze!

Do zobaczenia! 🚀

Przejdź do CodeWorlds