Skip to content

jorgenlt/photo-gallery

Repository files navigation

Photo Gallery

A photo portfolio site for photographers.


Visit site: photography.jorgenlt.no


Photo Gallery on desktop.



Photo Gallery on mobile.        Photo Gallery on mobile.

Features

  • Browse photos by category.
  • View photos in full size.
  • Upload new images to category of choice.

Technologies

The application is build with React on the Vite.js framework. The app combines Redux Toolkit , Redux Thunk, and selectors to manage the state and actions. Sign in functionality is handled with Firebase Authentication and uploaded photos are stored in Firebase Storage.

Installation

  1. Install the required dependencies using npm:

    npm install

Usage

  1. Start the application by running the following command:

    npm run dev

Technical challenges

The Redux slice

Setting up the Redux with Redux Toolkit to mangage the app.

// src/features/photos/photosSlice.jsx

import { createSlice, createSelector, createAsyncThunk } from '@reduxjs/toolkit'
import { photos } from './photosList'
import fetchImageUrls from '../../common/storage/fetchImageUrls'

const initialState = {
  status: 'idle',
  userSignedIn: false,
  error: null,
  loading: true,
  darkMode: false,
  photos: photos,
  filterQuery: '',
  filteredPhotos: []
}

// Asynchronous thunk to fetch image URLs.
export const fetchImageUrlsThunk = createAsyncThunk(
  'photos/fetchImageUrls',
  fetchImageUrls
)

export const photosSlice = createSlice({
  name: 'photos',
  initialState,
  reducers: {
    toggleDarkMode: state => {
      state.darkMode = !state.darkMode
    },
    // Filter the photos based on the category or location that includes 
    // the filter query.
    updateFilterQuery: (state, action) => {
      state.filterQuery = action.payload;
      console.log(\`filterQuery updated with: \${action.payload}\`);
      state.filteredPhotos = state.photos.filter(photo => 
        photo.category.includes(state.filterQuery) || 
        photo.location.includes(state.filterQuery)
      );
    },
    toggleLoading: state => {
      state.loading = false;
    },
    setUserSignedIn: (state, action) => {
      state.userSignedIn = action.payload;
    }
  },
  extraReducers: builder => {
    builder
      .addCase(fetchImageUrlsThunk.pending, state => {
        state.status = 'loading';
      })
      .addCase(fetchImageUrlsThunk.fulfilled, (state, action) => {
        state.status = 'succeeded';
        action.payload.forEach(image => {

           // Check if the photo already exists in the state.
          const exists = state.photos.some(p => p.id === image.id);
        
          // If the photo doesn't exist, add it to the photos array in the state.
          if (!exists) {
            state.photos.push(image);
          }
        
        });
      })
      .addCase(fetchImageUrlsThunk.rejected, (state, action) => {
        state.status = 'failed';
        state.error = \`
          The fetching of images failed with the following 
          error message: "\${action.error.message}"
        \`; 
      });
  },
})

// Selector for all photos
export const selectAllPhotos = createSelector(
  state => state.photos.photos,
  photos => photos
)

// Action creators are generated for each case reducer function
export const { 
  toggleDarkMode, 
  updateFilterQuery, 
  toggleLoading, 
  setUserSignedIn
} = photosSlice.actions

export default photosSlice.reducer

Fetch all image URL's from photos stored in Firebase Storage.

// src/common/storage/fetchImageUrls.js

import { getStorage, ref, listAll, getDownloadURL } from "firebase/storage";

const storage = getStorage();

// Returns an array of objects containing the image URLs, categories, and locations
const fetchImageUrls = async () => {
  
  const listRef = ref(storage);
  
  const res = await listAll(listRef);
  
  const urls = await Promise.all(
    res.items.map(async (item) => {
      const url = await getDownloadURL(item);
      const tags = item.name.split('--');
      const location = tags[0];
      const category = tags[1];

      return {
        id: item.name,
        src: url,
        category,
        location
      };
    })
  );

  return urls;
}

export default fetchImageUrls;

Folder structure

├── app
│   └── store.jsx
├── App.jsx
├── assets
├── common
│   ├── storage
│   │   ├── fetchImageUrls.js
│   │   └── uploadImage.js
│   └── utils
│       └── firebase.js
├── components
│   ├── auth
│   │   ├── AuthDetails.jsx
│   │   ├── SignIn.jsx
│   │   └── SignUp.jsx
│   ├── Dropdown.jsx
│   ├── Footer.jsx
│   ├── Nav.jsx
│   ├── NavMobile.jsx
│   └── UserOptions.jsx
├── features
│   └── photos
│       ├── ImageModal.jsx
│       ├── Photo.jsx
│       ├── Photos.jsx
│       ├── photosList.js
│       ├── photosSlice.jsx
│       └── UploadImage.jsx
├── main.jsx
└── styles
    ├── app.scss
    ├── components
    │   ├── _auth.scss
    │   ├── _dropdown.scss
    │   ├── _footer.scss
    │   ├── _index.scss
    │   ├── _nav-mobile.scss
    │   ├── _nav.scss
    │   └── _user-options.scss
    ├── config
    │   ├── _base.scss
    │   ├── _index.scss
    │   └── _variables.scss
    └── features
        ├── _image-modal.scss
        ├── _index.scss
        ├── _photos.scss
        └── _upload-image.scss

Upcoming features

  • Enable sign up.
  • A user can create their own personal portfolio.
  • A user can CRUD categories and sub-categories.
  • Visitors can like(vote) a picture.