# Project export: SlugSearch

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: Slugs lose stuff all the time, SlugSearch helps you find it.
- Devpost: https://devpost.com/software/slugsearch
- GitHub: https://github.com/waterball01/SlugSearch
- Video: https://www.youtube.com/embed/rjBk_O5vMvY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Beginner Hack)
- Team: 1 GitHub contributor(s) — waterball01 (1 commits)

## Devpost submission (written by the team)

### Inspiration

One week ago I lost my air pods on my walk back from the gym. Throughout the week I searched up and down the path I took looking for where it could have gone but I never saw my beloved earphones again. Losing things is common everywhere but a problem unique to Santa Cruz is someone returning the item, but the owner not knowing where the item was returned. With so many buildings, each with their own Lost and Found, it is too easy to lose items and never see them again.

### What it does

Instead of having a lost and found in each building, SlugSearch creates a centralized Lost and Found where students can post and find lost items.

### How we built it

We used Visual Studio Code to create a front-end in HTML, back-end in Python, Flask to connect front to back end, Css for formatting, and Javascript for a few functions.

### Challenges we ran into

We had three major challenges. The first was learning how to program in HTML because neither of us had used it before. The second challenge was using SQL for our database because neither of us used that either. The final challenge was figuring out how to connect the HTML front-end to the Python back-end.

### Accomplishments we're proud of

We are proud of how nice the user interface looks, how accessible the website is, and how much we learned over this experience.

### What we learned

We learned a lot of languages on the fly like HTML, CSS, and Javascript. We also learned how to connect front-end programming in HTML to back-end programming in Python programming using Flask.

### What's next

SlugSearch is implementing AI using Google Bard to allow users to only take a picture of the item and allow the AI to fill in all the information automatically, including; what the object is, and its description. We will also create a mobile app to make SlugSearch more accessible and convenient.

## README (from the GitHub repository)

# SlugSearch 

A lost and found web app built for UC Santa Cruz. Students can post lost or found
items with a photo, location, and category, and browse or filter listings to
reunite people with their belongings.

## Features

- Upload a photo of a lost or found item
- Tag items by campus location and category
- Filter the feed by location and/or category
- Claim an item to remove it once it's been returned
- SQLite database to persist all listings

## Supported Locations

Cowell, Stevenson, Crown, Merrill, College 9, JRL, Porter, Kresge, RCC, Oakes,
McHenry, S&E Library, Other

## Supported Categories

Headphones, Bottles, Jackets, Hats, Keys, Wallets, Other

## Tech Stack

- **Backend:** Python, Flask, Flask-SQLAlchemy
- **Database:** SQLite
- **Frontend:** HTML, CSS


## Detected evidence (automated analysis)

Indexed codebase: 6 recognized source files, 22 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- SQL (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (7 of 7)

```
instance/items.db
README.md
server.py
static/styles_add.css
static/styles.css
templates/add.html
templates/index.html
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Create README.md
- Add files via upload

## Key source files (fetched from GitHub, selected and truncated for size)

### server.py

```python
from flask import Flask, request, render_template, redirect, url_for, jsonify
import datetime
import os
from flask_sqlalchemy import SQLAlchemy
from werkzeug.utils import secure_filename
from flask import send_from_directory

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///items.db'
db = SQLAlchemy(app)

class Item(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    image_path = db.Column(db.String(255))
    location = db.Column(db.String(50))
    time = db.Column(db.String(20))
    category = db.Column(db.String(50))
    description = db.Column(db.Text)  # Add a description field

with app.app_context():
    db.drop_all()
    db.create_all()

# Get unique locations from items
locations = ['Cowell', "Stevenson", "Crown", "Merril", "College 9", "JRL", "Porter", "Kresge", "RCC", "Oakes", "McHenry", "S&E Library", "Other"]
categories = ['Headphones', 'Bottles', 'Jackets', 'Hats', "Keys", "Wallets", "Other"]

@app.route("/", methods=['GET', 'POST'])
def index():
    selected_location = request.form.get('location')
    selected_category = request.form.get('category')

    if selected_location == 'all':
        selected_location = None

    if selected_category == 'all':
        selected_category = None

    if selected_location and selected_category:
        filtered_items = Item.query.filter_by(location=selected_location, category=selected_category).all()
    elif selected_location:
        filtered_items = Item.query.filter_by(location=selected_location).all()
    elif selected_category:
        filtered_items = Item.query.filter_by(category=selected_category).all()
    else:
        filtered_items = Item.query.all()
    

    return render_template("index.html", items=filtered_items, locations=locations, categories=categories,
                           selected_location=selected_location, selected_category=selected_category)


@app.route("/add")
def add():
    return render_template('add.html', locations=locations, categories=categories) # Create a new HTML file for the "add" page

@app.route('/upload', methods=['POST'])
def upload():
    if 'file' not in request.files:
        return "No file part"

    file = request.files['file']

    if file.filename == '':
        return "No selected file"

    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(file_path)

        # Get other form data
        location = request.form.get('selected_location')
        category = request.form.get('selected_category')
        description = request.form.get('description')  # Get description data
        current_datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # Save to database
        new_item = Item(image_path=file_path, location=location, time=current_datetime, category=category, description=description)
        db.session.add(new_item)
        db.session.commit()

        return redirect(url_for('index'))

    return "Invalid file type"

@app.route('/uploads/<filename>')
def uploaded_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

@app.route('/claim/<int:item_id>', methods=['POST'])
def claim_item(item_id):
    # Find the item by ID
    item = Item.query.get(item_id)

    if item:
        # Remove the item from the database
        db.session.delete(item)
        db.session.commit()
        return jsonify({'success': True})
    else:
        return jsonify({'success': False, 'message': 'Item not found'}), 404

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'}


def is_valid_image(file_path):
    # Add your image validation logic here
    # Example: check if the file is an actual image
    return True


def is_valid_location(location):
    return location in locations


def is_valid_category(category):
    return category in categories


if __name__ == '__main__':
    app.run(debug=True)

```

### templates/index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
    <title>SlugSearch</title>
    <script>
        function claimItem(itemId) {
            // Make an AJAX request to the server to handle the claim
            fetch('/claim/' + itemId, { method: 'POST' })
                .then(response => {
                    if (response.ok) {
                        // Reload the page after successful claim
                        location.reload();
                    }
                })
                .catch(error => console.error('Error claiming item:', error));
        }
    </script>
</head>
<body>
    <header class="header">
        <form method="post" action="/">
            <div class="dropdown">
                <div class="menu-btn">
                    <img src="{{ url_for('static', filename='745228.png') }}" alt="Menu">
                    <span class="menu-text">Location</span>
                </div>
                <div class="dropdown-content">
                    {% for location in locations %}
                        <button type="submit" name="location" value="{{ location }}">{{ location }}</button>
                    {% endfor %}
                    <button type="submit" name="location" value="all">All Locations</button>
                </div>
            </div>
            <div class="dropdown">
                <div class="menu-btn">
                    <img src="{{ url_for('static', filename='cate.png') }}" alt="Menu">
                    <span class="menu-text">Category</span>
                </div>
                <div class="dropdown-content">
                    {% for category in categories %}
                        <button type="submit" name="category" value="{{ category }}">{{ category }}</button>
                    {% endfor %}
                    <button type="submit" name="category" value="all">All Categories</button>
                </div>
            </div>
        </form>        
        <div class="slugsearch">SlugSearch</div>
        <!-- Add this button inside the header -->
        <a href="{{ url_for('add') }}" class="add-button"><strong>+</strong></a>
    </header>
    <div class="items">
        {% for item in items %}
            <div class="item" onmouseover="showClaimButton(this)" onmouseout="hideClaimButton(this)">
                <img src="{{ url_for('uploaded_file', filename=item.image_path.split('/')[-1]) }}" alt="Item Image">
                <p><strong>Location:</strong> {{ item['location'] }}</p>
                <p><strong>Time:</strong> {{ item['time'] }}</p>
                <p><strong>Category:</strong> {{ item['category'] }}</p>
                <!-- Display the description at the bottom of the card -->
                <p><strong>Description:</strong> {{ item['description'] }}</p>
                <button class="claim-button" onclick="claimItem({{ item['id'] }})"><strong>Claim</strong></button>
            </div>
        {% endfor %}
        {% for _ in range(4) %}
            <div class="fake-card"></div>
        {% endfor %}
    </div>
</body>
</html>

```

### static/styles.css

```css
body {
    margin: 0;
    font-family: 'Futura', sans-serif;
}

.header {
    background-color: #ffd801; /* Light yellow color */
    padding: 20px;
    padding-left: 10px;
    display: flex;
    justify-content: space-between; /* Align items with space between them */
    align-items: center; /* Center items vertically */
    position: fixed;
    width: 100%;
    top: 0;
    z-index: 1000;
    font-family: 'Futura', sans-serif;
}

.menu-btn {
    display: flex;
    align-items: center;
    cursor: pointer;
}

.menu-btn img {
    width: 30px;
    height: auto;
    margin-right: 8px; /* Adjust the spacing between icon and text */
}

.menu-text {
    font-size: 18px; /* Adjust font size for the text */
}

.slugsearch {
    flex-grow: 1; /* Allow SlugSearch to take remaining space */
    text-align: center; /* Center the text within the available space */
    margin-left: -177px;
    font-size: 36px;
}

.dropdown {
    position: relative;
    display: inline-block;
    margin: 5px;
}

.dropdown-content {
    display: none;
    position: absolute;
    background-color: #fff;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); /* Box shadow for dropdown */
    z-index: 1001;
    width: 200%;
}

.dropdown-content button {
    background-color: transparent;
    border: none;
    color: black;
    width: 100%; /* Make the button full width of the dropdown */
    padding: 12px 16px;
    display: block;
    text-decoration: none;
    cursor: pointer;
    font-family: 'Futura', sans-serif; /* Match the font-family with the menu */
    font-size: 18px; /* Match the font size with the menu */
    text-align: left
}

.dropdown-content button:hover {
    background-color: #f0f0f0; /* Highlight color on hover */
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), 0 0 0 2px #f0f0f0; /* Full-width shadow on hover */
}

.dropdown:hover .dropdown-content {
    display: block;
}

/* Add additional styles for content below the header */

.items {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
    max-width: 100%;
    margin: 50px;
    margin-top: 100px;
    padding: 20px;
}

.item {
    border: 1px solid #ccc;
    border-radius: 10px;
    padding: 15px;
    margin: 35px; /* Edge margins of 70px */
    width: calc(25% - 70px); /* 25% width with 70px margin on each side */
    text-align: center;
    transition: box-shadow 0.3s ease-in-out;
    box-sizing: border-box;
    position: relative;
}

.item:hover {
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); /* Larger shadow on hover */
    border-radius: 10px 10px 0 0;

}

img {
    width: 100%; /* Make the image fill the container */
    height: 200px; /* Fixed height for the image */
    object-fit: cover; /* Maintain aspect ratio and cover the container */
    border-radius: 8px; /* Round the edges of the image */
}

/* Fake card styles */
.fake-card {
    border: 1px solid #ccc;
    border-radius: 10px;
    padding: 15px;
    margin: 35px; /* Edge margins of 70px */
    width: calc(25% - 70px); /* 25% width with 70px margin on each side */
    text-align: center;
    transition: box-shadow 0.3s ease-in-out;
    box-sizing: border-box;
    visibility: hidden;
}

.claim-button {
    display: none; /* Initially hide the button */
    position: absolute;
    top: 100%; /* Align the top of the button with the bottom of the card */
    left: -1px;
    width: calc(100% + 2px); /* Match the width of the card */
    padding: 10px;
    background-color: #fff; /* White color */
    color: #000; /* Black text color, you can customize this */
    border: 1px solid #ccc;
    transition: box-shadow 0.3s ease-in-out;
    border-radius: 0px 0px 5px 5px; /* Rounded corners only at the bottom */
    cursor: pointer;
    box-sizing: border-box; /* Include padding and border in the width */
    font-size: 15px;
}

.item:hover .claim-button {
    display: block; /* Show the button on hover */
    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}

.add-button {
    display: inline-block;
    font-size: 32px; /* Increase the font size */
    text-align: center;
    text-decoration: none;
    color: #000;
    padding: 10px;
    margin-right: 30px; /* Adjust the spacing from the SlugSearch */
    width: 40px; /* Set the width to make it a square */
    height: 40px; /* Set the height to make it a square */
    line-height: 40px; /* Center the text vertically */
    border-radius: 50%;
    border: 3px solid #000;
    background-color: #fff;
    transition: box-shadow 0.3s ease-in-out;
}

.add-button:hover {
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.4); /* Shadow on hover */
}


@media only screen and (max-width: 600px) {
    .menu-btn {
        order: -1; /* Move the menu to the leftmost position */
    }

    .dropdown-content {
        width: 100%; /* Full width for small screens */
    }
}
```

### templates/add.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles_add.css') }}">
    <title>SlugSearch</title>
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            const locationDropdown = document.getElementById('locationDropdown');
            const selectedLocation = document.getElementById('selectedLocation');
            const categoryDropdown = document.getElementById('categoryDropdown');
            const selectedCategory = document.getElementById('selectedCategory');
            const selectedLocationInput = document.getElementById('selectedLocationInput');
            const selectedCategoryInput = document.getElementById('selectedCategoryInput');
            const fileInput = document.getElementById('file');
            const selectedFile = document.getElementById('selectedFile');
            const selectedImage = document.getElementById('selectedImage');
            const fileLabel = document.querySelector('.file-label');

            locationDropdown.addEventListener('click', function (event) {
                if (event.target.tagName === 'BUTTON') {
                    selectedLocation.textContent = event.target.textContent;
                    selectedLocationInput.value = event.target.textContent; // Update the hidden input
                }
            });

            categoryDropdown.addEventListener('click', function (event) {
                if (event.target.tagName === 'BUTTON') {
                    selectedCategory.textContent = event.target.textContent;
                    selectedCategoryInput.value = event.target.textContent; // Update the hidden input
                }
            });

            fileInput.addEventListener('change', function () {
                const file = fileInput.files.length > 0 ? fileInput.files[0] : null;

                if (file) {
                    selectedFile.textContent = file.name;
                    const reader = new FileReader();

                    reader.onload = function (e) {
                        selectedImage.src = e.target.result;
                    };

                    reader.readAsDataURL(file);
                } else {
                    selectedFile.textContent = 'Choose a File';
                    selectedImage.src = ''; // Clear the image source
                }
            });

            selectedFile.addEventListener('click', function () {
                fileInput.click(); // Trigger the file input click when the label is clicked
            });
        });
    </script>
</head>
<body>
    <header class="header">
        <a class="hide"><strong>+</strong></a>
        <div class="slugsearch">SlugSearch</div>
        <a href="{{ url_for('index') }}" class="add-button">Search</a>
    </header>
    <div class="container">
        <h1 class="title">Upload Missing Item</h1>
        <div class="rectangular-box">
            <img id="selectedImage">
        </div>
        <form action="/upload" method="post" enctype="multipart/form-data">
            <div class="dropdown">
                <label for="file">Select Image:</label>
                <div class="selected-file" id="selectedFile" for="file">Choose a File</div>
                <input type="file" name="file" id="file" required class="file-input">
            </div><br><br>

            <div class="dropdown">
                <label for="location">Selected Location:</label>
                <div class="selected-location" id="selectedLocation">Select a Location</div>
                <div class="dropdown-content" id="locationDropdown">
                    {% for loc in locations %}
                        <button type="button">{{ loc }}</button>
                    {% endfor %}
                </div>
            </div><br><br>

            <div class="dropdown">
                <label for="category">Selected Category:</label>
                <div class="selected-category" id="selectedCategory">Select a Category</div>
                <div class="dropdown-content" id="categoryDropdown">
                    {% for cat in categories %}
                        <button type="button">{{ cat }}</button>
                    {% endfor %}
                </div>
            </div><br><br>

            <div class="description-box">
                <label for="description">Description:</label>
                <textarea name="description" id="descriptionInput" rows="4" cols="50" placeholder="Enter a description..."></textarea>
            </div><br><br>
            <input type="hidden" name="selected_location" id="selectedLocationInput" value="">
            <input type="hidden" name="selected_category" id="selectedCategoryInput" value="">
            <input type="submit" value="Upload" class="upload-button" id="uploadButton">
        </form>
        
    </div>
</body>
</html>
```

### static/styles_add.css

```css
body {
    margin: 0;
    font-family: 'Futura', sans-serif;
    border-top: 120px solid transparent;
}

.header {
    background-color: #ffd801; /* Light yellow color */
    padding: 20px;
    padding-left: 10px;
    display: flex;
    justify-content: space-between; /* Align items with space between them */
    align-items: center; /* Center items vertically */
    position: fixed;
    width: 100%;
    top: 0;
    z-index: 1000;
    font-family: 'Futura', sans-serif;
}

.slugsearch {
    flex-grow: 1; /* Allow SlugSearch to take remaining space */
    text-align: center; /* Center the text within the available space */
    margin-left: 0px;
    font-size: 36px;
}

.add-button {
    display: inline-block;
    font-size: 18px; /* Increase the font size */
    text-align: center;
    text-decoration: none;
    color: #000;
    padding: 10px;
    margin-right: 30px; /* Adjust the spacing from the SlugSearch */
    border-radius: 5px;
    border: 3px solid #000;
    background-color: #fff;
    transition: box-shadow 0.3s ease-in-out;
}

.add-button:hover {
    box-shadow: 0 0 20px rgba(0, 0, 0, 0.4); /* Shadow on hover */
}
.hide {
    display: inline-block;
    font-size: 32px; /* Increase the font size */
    text-align: center;
    text-decoration: none;
    color: #000;
    padding: 10px;
    margin-right: 30px; /* Adjust the spacing from the SlugSearch */
    width: 40px; /* Set the width to make it a square */
    height: 40px; /* Set the height to make it a square */
    line-height: 40px; /* Center the text vertically */
    border-radius: 50%;
    border: 3px solid #000;
    background-color: #fff;
    transition: box-shadow 0.3s ease-in-out;
    visibility: hidden;
}

.dropdown {
    position: relative;
    display: inline-block;
    margin-right: 100px; /* Move the dropdown to the right */
    margin-bottom: 15px;
    margin-left: 10px;
    align-items: center;
    justify-content: center;
}

.dropdown-content {
    display: none;
    position: absolute;
    background-color: #fff;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    z-index: 1001;
    width: 200%;
    margin-left: 130px;
}

.dropdown-content button {
    background-color: transparent;
    border: none;
    color: black;
    width: 100%;
    padding: 12px 16px;
    display: block;
    text-decoration: none;
    cursor: pointer;
    font-family: 'Futura', sans-serif;
    font-size: 18px;
    text-align: left;
}

.dropdown-content button:hover {
    background-color: #f0f0f0;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), 0 0 0 2px #f0f0f0;
}

.dropdown:hover .dropdown-content {
    display: block;
}

.title {
    text-align: center;
    margin: 10px;
    font-size: 32px;
}

.container {
    margin: 0 auto; /* Center-align the container */
    max-width: 800px; /* Set a maximum width for better readability */
}

.rectangular-box {
    height: 400px; /* Adjust the height as needed */
    border: 5px solid #000;
    border-radius: 15px; /* Rounded corners */
    margin-bottom: 20px; /* Spacing between the box and the text */
    overflow: hidden;
}

#selectedImage {
    display: block; /* Make the image a block element for centering */
    margin: auto; /* Center the image horizontally */
    height: 400px;
}

.selected-location,
.selected-category {
    display: inline-block;
    font-size: 18px;
    text-align: left;
    padding: 10px;
    margin-right: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    font-family: 'Futura', sans-serif;
    background-color: transparent;
}

.selected-location:hover,
.selected-category:hover,
.upload-button:hover {
    background-color: #f0f0f0;
}

.upload-button {
    display: inline-block;
    font-size: 24px;
    text-align: left;
    padding: 10px;
    margin-right: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    font-family: 'Futura', sans-serif;
    background-color: transparent;
    position: fixed;
    bottom: 10px;
    right: 10px;
    z-index: 999; /* Ensure a higher z-index */
}


.file-input {
    display: none; /* Hide the default file input */
}

.selected-file {
    display: inline-block;
    font-size: 18px;
    text-align: left;
    padding: 10px;
    margin-right: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
}

.selected-file:hover {
    background-color: #f0f0f0;
}

.description-box {
    margin-bottom: 15px;
}

.description-box label {
    display: block;
    font-size: 18px;
    margin-bottom: 5px;
}

.description-box textarea {
    width: 100%;
    padding: 10px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 5px;
    box-sizing: border-box;
    resize: vertical;
}

/* Style to match the upload button */
.description-box textarea:focus {
    outline: none;
    border: 2px solid #000; /* Match the border color of the upload button */
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* Match the box-shadow of the upload button */
}

.description-box textarea:hover {
    background-color: #f0f0f0;
}
```