<repo_name>dkapur17/Spot-o-Matic<file_sep>/frontend/src/components/NewSearch.jsx
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { useHistory } from 'react-router-dom';
import TinderCard from 'react-tinder-card';
import { nanoid } from 'nanoid'
import axios from 'axios';
import swal from 'sweetalert';

const NewSearch = () => {
    return (
        <motion.div
            className='container mt-2 justify-content-center'
            initial={{ y: 100, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            exit={{ y: 100, opacity: 0 }}
            transition={{ duration: 1 }}
        >
            <h1 className='display-1 text-center text-danger mb-2' id='title'>Help Us Help You!</h1>
            <p className='lead text-danger text-center'>Swipe RIGHT if you're interested or LEFT if you're not.</p>
            <CardsWrapper />
        </motion.div>
    )
};

const CardsWrapper = () => {

    const lists = {
        'Watch Movies': [],
        'Leisure Time': ['Picnic Spots', 'Museums', 'Art Galleries', 'Historic Monuments'],
        'Games/Sports': ['Indoor Games', 'Trekking/Camping'],
        'Clubbing': ['Drinking', 'Dancing'],
        'Eating Out': ['Indian Cuisine', 'Chinese Cuisine', 'Fast Food', 'Continental Cuisine']
    };

    const superSections = ['Watch Movies', 'Leisure Time', 'Games/Sports', 'Clubbing', 'Eating Out']

    const [cardData, setCardData] = useState([
        ...lists['Leisure Time'],
        ...lists['Games/Sports'],
        ...lists['Clubbing'],
        ...lists['Eating Out'],
        ...superSections
    ]);

    const [preferences, setPreferences] = useState({
        id: nanoid(10),
        eatingOut: {
            score: 0,
            variants: ''
        },
        clubbing: {
            score: 0,
            variants: ''
        },
        playingGames: {
            score: 0,
            variants: ''
        },
        watchingMovies: {
            score: 0,
            variants: ''
        },
        leisureTime: {
            score: 0,
            variants: ''
        },
    });

    const [loading, setLoading] = useState(false);

    const history = useHistory();

    useEffect(() => {

        const sendPreferences = async () => {
            try {
                setLoading(true);
                const res = await axios.post('http://localhost:5000/setPreferences', preferences);
                console.log(res.data);
                console.log(preferences);
                setLoading(false);
                history.push(`/showLink/${preferences.id}`);
            }
            catch {
                swal("Error!", "Unable to write to the Database. Please try again later.", "error");
                history.push('/');
            }
        }

        if (!cardData.length)
            sendPreferences();
    }, [cardData, preferences, history]);

    const handleCardLeavingScreen = (direction, item) => {
        if (superSections.includes(item)) {
            if (direction === 'left')
                setCardData((currentCardData) => currentCardData.filter(cardItem => cardItem !== item && !lists[item].includes(cardItem)));
            else {
                setCardData((currentCardData) => currentCardData.filter(cardItem => cardItem !== item));
                switch (item) {
                    case "Eating Out":
                        setPreferences((currentPreferences) => ({ ...currentPreferences, eatingOut: { score: currentPreferences.eatingOut.score + 1, variants: currentPreferences.eatingOut.variants } }));
                        break;
                    case "Clubbing":
                        setPreferences((currentPreferences) => ({ ...currentPreferences, clubbing: { score: currentPreferences.clubbing.score + 1, variants: currentPreferences.clubbing.variants } }));
                        break;
                    case "Games/Sports":
                        setPreferences((currentPreferences) => ({ ...currentPreferences, playingGames: { score: currentPreferences.playingGames.score + 1, variants: currentPreferences.playingGames.variants } }));
                        break;
                    case "Leisure Time":
                        setPreferences((currentPreferences) => ({ ...currentPreferences, leisureTime: { score: currentPreferences.leisureTime.score + 1, variants: currentPreferences.leisureTime.variants } }));
                        break;
                    case "Watching Movies":
                        setPreferences((currentPreferences) => ({ ...currentPreferences, watchingMovies: { score: currentPreferences.watchingMovies.score + 1, variants: currentPreferences.leisureTime.variants } }));
                        break;
                    default:
                        break;
                }
            }
        }
        else {
            setCardData((currentCardData) => currentCardData.filter(cardItem => cardItem !== item));
            if (direction === 'right') {
                if (lists["Eating Out"].includes(item))
                    setPreferences((currentPreferences) => ({ ...currentPreferences, eatingOut: { score: currentPreferences.eatingOut.score, variants: currentPreferences.eatingOut.variants + item + '-' } }));
                else if (lists["Clubbing"].includes(item))
                    setPreferences((currentPreferences) => ({ ...currentPreferences, clubbing: { score: currentPreferences.clubbing.score, variants: currentPreferences.clubbing.variants + item + '-' } }));
                else if (lists["Games/Sports"].includes(item))
                    setPreferences((currentPreferences) => ({ ...currentPreferences, playingGames: { score: currentPreferences.playingGames.score, variants: currentPreferences.playingGames.variants + item + '-' } }));
                else if (lists["Leisure Time"].includes(item))
                    setPreferences((currentPreferences) => ({ ...currentPreferences, leisureTime: { score: currentPreferences.leisureTime.score, variants: currentPreferences.leisureTime.variants + item + '-' } }));
                else if (lists["Watch Movies"].includes(item))
                    setPreferences((currentPreferences) => ({ ...currentPreferences, watchingMovies: { score: currentPreferences.watchingMovies.score, variants: currentPreferences.watchingMovies.variants + item + '-' } }));
            }

        }
    }

    return loading ?
        <div className="row justify-content-center mt-5">
            <div className="spinner-border text-danger" role="status" style={{ width: "5rem", height: "5rem" }}>
                <span className="sr-only">Loading...</span>
            </div>
        </div>
        : (
            <div
                className='cardContainer row justify-content-center mt-5'>
                {cardData.map((item, i) =>
                    <TinderCard
                        key={Math.random()}
                        className='swipe'
                        preventSwipe={['up', 'down']}
                        onCardLeftScreen={(direction) => handleCardLeavingScreen(direction, item)}
                    >
                        <div className="card text-white bg-danger mb-3" style={{ width: "40rem", height: "30rem" }}>
                            <div className="card-header text-center">
                                <p className="lead">Do you like...</p>
                            </div>
                            <div className="card-body d-flex flex-column justify-content-center">
                                <h1 className="card-title display-1 text-center">{item}</h1>
                            </div>
                        </div>
                    </TinderCard>
                )}
            </div>
        )
}

export default NewSearch;<file_sep>/frontend/src/App.jsx
import { Switch, Route, useLocation } from 'react-router-dom'
import { AnimatePresence } from 'framer-motion'

import 'bootstrap/dist/js/bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
import 'jquery'
import 'popper.js'

import './App.css'
import Home from './components/Home'
import New from './components/New'
import Continue from './components/Continue'
import NewSearch from './components/NewSearch'
import ShowCode from './components/ShowCode'
import ContinueSearch from './components/ContinueSearch';
import DisplayResults from './components/DisplayResults'


const App = () => {
  const location = useLocation();
  return (
    <AnimatePresence exitBeforeEnter>
      <Switch location={location} key={location.pathname}>
        <Route path='/' exact component={Home} />
        <Route path='/new' exact component={New} />
        <Route path='/continue' exact component={Continue} />
        <Route path='/search/new' exact component={NewSearch} />
        <Route path='/search/continue/:code' exact component={ContinueSearch} />
        <Route path='/showLink/:code' exact component={ShowCode} />
        <Route path='/results' exact component={DisplayResults} />
      </Switch>
    </AnimatePresence>
  )
};

export default App;<file_sep>/backend/server.js
const fs = require('fs');
const express = require('express');
const queries = require('./queries');
const cors = require('cors');

require('dotenv').config()

const PORT = 5000

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }))
app.use(cors());

app.post('/setPreferences', queries.setPreferences);
app.post('/getPreferences', queries.getPreferences);

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`)
});<file_sep>/README.md
# Spot-💖-Matic

![Banner](README_ASSETS/Banner.png)

*Submission for Hackthrob 2021*
<hr>

### Introduction

Finding love is tough, but finding things to do on your next date is even tougher. Well, no more! Introducing **Spot-o-Matic**. 

Spot-o-Matic takes into account your and your partner's preferences and suggests SPOTS for your next date.
The UI is inspired by the universally beloved Tinder swipe-style.

![Swipe Demo GIF](README_ASSETS/swipeDemo.gif)


The preferences of both partners are taken into account and passed through our *sophisticated* algorithm, that then rates the different activities based on your interests in them and then provides you a list of places on Google Maps that it thinks you would like to spend your next date at.

<!-- ![Google Maps Embeds](README_ASSETS/mapEmbed.gif) -->


### How to use it

It starts with one partner looking for something to do on their next date. They open up Spot-o-Matic and start a new search. They then provide their interests. Once that's done, they are given a 10 character long code. They share this code with their partner. The partner now opens Spot-o-Matic and continues the search by providing the code they recieved. They too provide their interests. The collection of the two sets of interests are evaluated and the potential SPOTs of interest are shown.

### Getting Technical

The UI is created in React. It interfaces with a CockroachDB database that stores the user preferences through an Express backed. Maps were made possible by the google-maps API.
<file_sep>/frontend/src/components/DisplayResults.jsx
import { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { motion } from 'framer-motion'


const iframeStyle = {
    border: "3px solid #d9534f",
    borderRadius: "10px"
};

const DisplayResults = (props) => {
    const [numMaps, setNumMaps] = useState(1)
    const [mapQuery, setMapQuery] = useState([])

    const sortScore = (a, b) => {
        return b.score - a.score;
    }

    const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0);
    const history = useHistory();

    const filterQueries = (act) => {
        const variants = act.variants.split('-');

        let max_occur = { key: '', freq: -1 };

        for (let i = 0; i < variants.length; i++) {
            let count = countOccurrences(variants, variants[i])
            if (count > max_occur.freq)
                max_occur = { key: variants[i], freq: count }
        }

        if (max_occur.freq === 1) {
            if (variants.length > 1) {
                setNumMaps(2);
                let query1 = variants[0].split(' ').join('+');
                let query2 = variants[1].split(' ').join('+');

                setMapQuery([query1, query2])

            } else {
                let query1 = variants[0].split(' ').join('+');
                setMapQuery([query1])
            }
        } else {
            let query1 = max_occur.key.split(' ').join('+');
            setMapQuery([query1])
        }
    }

    useEffect(() => {
        const Analyzer = () => {
            let raw = props.location.state.preferences
            let data = [{ key: 'eatingOut', ...raw.eatingOut },
            { key: 'leisureTime', ...raw.leisureTime },
            { key: 'playingGames', ...raw.playingGames },
            { key: 'clubbing', ...raw.clubbing },
            { key: 'watchingMovies', ...raw.watchingMovies }]

            data.sort(sortScore)

            const best = data.filter(activity => activity.score >= 2)
            if (best !== []) {
                filterQueries(best[0]);
                return;
            }
            const med = data.filter(activity => activity.score === 1)
            if (med !== []) {
                filterQueries(med[0]);
                return;
            }
        }

        Analyzer();
    }, [])

    return (
        <motion.div
            className='container my-5 justify-content-center'
            initial={{ y: 100, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            exit={{ y: 100, opacity: 0 }}
            transition={{ duration: 1 }}
        >
            <h1 id='title' className='text-danger text-center display-3 mb-5'>Spot-💖-Matic suggests...</h1>
            {mapQuery.length === 0 ?
                (
                    <div className="d-flex justify-content-center" style={{ marginTop: '20rem' }}>
                        <div className="spinner-grow text-primary" role="status">
                            <span className="sr-only">Loading...</span>
                        </div>
                    </div>
                ) : numMaps === 1 ? (
                    <div className='row justify-content-center'>
                        <div>
                            <iframe width="500" height="400" frameBorder="0" style={iframeStyle} title={`${mapQuery[0]}`} className="embed-responsive-item" src={`https://www.google.com/maps/embed/v1/search?q=${mapQuery[0]}+near+me&key=<KEY>`} allowFullScreen></iframe>
                            <p className="text-danger text-center lead">SUGGESTED SPOTS: {mapQuery[0].split('+').join(' ').toUpperCase()}</p>
                        </div>
                    </div>
                ) : (
                        <div className='row justify-content-around'>
                            <div className="col">
                                <iframe width="500" height="400" frameBorder="0" style={iframeStyle} title={`${mapQuery[0]}`} className="embed-responsive-item" src={`https://www.google.com/maps/embed/v1/search?q=${mapQuery[0]}+near+me&key=<KEY>`} allowFullScreen></iframe>
                                <p className="text-danger text-center lead">SUGGESTED SPOTS: {mapQuery[0].split('+').join(' ').toUpperCase()}</p>
                            </div>
                            <div className="col">
                                <iframe width="500" height="400" frameBorder="0" style={iframeStyle} title={`${mapQuery[1]}`} className="embed-responsive-item" src={`https://www.google.com/maps/embed/v1/search?q=${mapQuery[1]}+near+me&key=<KEY>`} allowFullScreen></iframe>
                                <p className="text-danger text-center lead">SUGGESTED SPOTS: {mapQuery[1].split('+').join(' ').toUpperCase()}</p>
                            </div>
                        </div>
                    )
            }
            <div className="row justify-content-center">
                < button className="love-button" onClick={() => history.push('/')}>Another Search?</button>
            </div>
        </motion.div >
    )
}

export default DisplayResults;<file_sep>/frontend/src/components/New.jsx
import { useHistory } from 'react-router-dom';
import { motion } from 'framer-motion';

const New = () => {

    const history = useHistory();

    const handleSubmit = () => {
        history.push('/search/new')
    };

    return (
        <motion.div
            className='container my-5 justify-content-center'
            initial={{ y: 100, opacity: 0 }}
            animate={{ y: 0, opacity: 1 }}
            exit={{ y: 100, opacity: 0 }}
            transition={{ duration: 1 }}
        >
            <h1 className="display-1 text-center text-danger mb-5" id="title">New Search</h1>
            <p className="lead text-danger text-center">Finding the perfect partner is tough. But whats even tougher is finding fun things to do together!
            Spot-O-Matic takes into account your and your partner's interests and tries to suggest activites fo you to try out.
            It even provides you locations (or SPOTS) to visit near you, where you can have fun with your better half.
                On each card, swipe <strong>RIGHT 👉</strong> if you are interested in what the card says or swipe <strong>LEFT 👈</strong> if you're not.
                Just let us know your interests and we will help you find the perfect SPOT for your next date 😉. </p>
            <div className="row justify-content-center mt-5">
                <button className="love-button" onClick={handleSubmit}>Lets Do This!</button>
            </div>
        </motion.div>
    )
}

export default New;