Skip to content

Instantly share code, notes, and snippets.

@eterps
Last active December 12, 2024 15:20
Show Gist options
  • Save eterps/10f04df57a4f766e54eb2610581c91bb to your computer and use it in GitHub Desktop.
Save eterps/10f04df57a4f766e54eb2610581c91bb to your computer and use it in GitHub Desktop.
Select
require 'sinatra'
require 'json'
TECH_TAGS = %w[javascript python ruby java rust golang react vue angular typescript]
get('/') { erb :index }
get('/suggest') { TECH_TAGS.grep(/#{params[:q]}/i).to_json }
get('/search') { erb :_results, layout: false, locals: { tags: params[:tags].to_s.split(',').reject(&:empty?) } }
__END__
@@ layout
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tech Filter</title>
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('techFilter', () => ({
search: '',
suggestions: [],
selected: new URLSearchParams(location.search).get('tags')?.split(',').filter(Boolean) || [],
results: '',
async fetchSuggestions() {
this.suggestions = this.search ?
await (await fetch(`/suggest?q=${this.search}`)).json() : [];
},
updateUrl() {
const url = this.selected.length ? `/?tags=${this.selected.join(',')}` : '/';
history.pushState(null, '', url);
},
async addTag(tag) {
if (!this.selected.includes(tag)) {
this.selected.push(tag);
this.updateUrl();
this.results = await (await fetch(`/search?tags=${this.selected}`)).text();
}
this.search = '';
this.suggestions = [];
},
removeTag(tag) {
this.selected = this.selected.filter(t => t !== tag);
this.updateUrl();
this.updateResults();
},
async updateResults() {
this.results = await (await fetch(`/search?tags=${this.selected}`)).text();
},
init() {
if (this.selected.length) this.updateResults();
}
}));
});
</script>
<style>
body { display: flex; margin: 0 }
aside {
width: 250px;
padding: 1rem;
border-right: 1px solid #ddd;
height: 100vh
}
main {
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column
}
input {
width: 100%;
margin-bottom: 1rem;
padding: .5rem
}
.tag {
display: flex;
padding: .5rem 0
}
.remove {
margin-left: auto;
border: 0;
background: 0;
cursor: pointer
}
#suggestions {
position: absolute;
background: #fff;
border: 1px solid #ddd;
width: 250px
}
.suggestion { padding: .5rem }
.suggestion:hover { background: #f5f5f5 }
.results-header {
text-align: right;
margin-bottom: 1rem
}
</style>
</head>
<body>
<%= yield %>
</body>
</html>
@@ index
<div x-data="techFilter">
<aside>
<h3>Tech Stack</h3>
<div class="search-container">
<input type="text"
x-model="search"
@input.debounce.200ms="fetchSuggestions"
@click.outside="suggestions = []"
placeholder="Search technologies...">
<div id="suggestions" x-show="suggestions.length">
<template x-for="tag in suggestions">
<div class="suggestion"
x-text="tag"
@click="addTag(tag)">
</div>
</template>
</div>
</div>
<template x-for="tag in selected">
<div class="tag">
<span x-text="tag"></span>
<button class="remove" @click="removeTag(tag)">×</button>
</div>
</template>
</aside>
<main x-html="results">
<h1>Results will appear here</h1>
</main>
</div>
@@ _results
<div class="results-header">
<h2>Results for tags: <%= tags.join(', ') %></h2>
</div>
<div class="results-content">
<!-- Your actual results rendering here -->
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment