Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.device-card {
margin-bottom: 1rem;
border: 1px solid var(--pico-border-color);
border-radius: 8px;
padding: 1rem;
background: var(--pico-background-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.device-card h3 {
margin-top: 0;
color: var(--pico-primary);
}

.dimensions {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}

.dimension-box {
display: flex;
align-items: center;
justify-content: center;
background: #e9ecef;
border: 1px solid #dee2e6;
border-radius: 4px;
}

.specs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-top: 1rem;
}

.spec {
font-size: 0.9rem;
}

.spec strong {
color: var(--pico-muted-color);
}

#device-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100%, 1fr));
gap: var(--pico-grid-column-gap);
}

#filter-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

#filter-list-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

#filter-dropdown-container {
min-width: 100%;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

#filter-dropdown-container label {
display: flex;
flex-direction: column;
}

#sort-container {
display: flex;
gap: 1rem;
align-items: end;
margin-bottom: 1rem;
}

[data-attribute="ram"],
[data-attribute="cpu"] {
text-transform: uppercase;
}

div[class^="-button"] {
padding: 1px;
}
2 changes: 1 addition & 1 deletion data.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
model,brand,height,width,depth,screen_size,launch_year,operating_system,manufacturer_support,community_support,cpu,ram,battery
model,brand,height,width,depth,screen-size,launch-year,operating-system,manufacturer-support,community-support,cpu,ram,battery
iPhone SE (2016),Apple,123.8,58.6,7.6,4,2016,iOS 15.8.5,No,No,Apple A9,2,1624
iPhone 6S,Apple,138.3,67.1,7.1,4.7,2015,iOS 15.8.5,No,No,Apple A9,2,1715
iPhone 7,Apple,138.3,67.1,7.1,4.7,2016,iOS 15.8.5,No,No,Apple A10 Fusion,2,1960
Expand Down
245 changes: 45 additions & 200 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,212 +1,57 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Small Phones <150 mm</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<style>
.device-card {
margin-bottom: 1rem;
border: 1px solid var(--pico-border-color);
border-radius: 8px;
padding: 1rem;
background: var(--pico-background-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.device-card h3 { margin-top: 0; color: var(--pico-primary); }
.dimensions { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.dimension-box { display: flex; align-items: center; justify-content: center; background: #e9ecef; border: 1px solid #dee2e6; border-radius: 4px; }
.specs { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-top: 1rem; }
.spec { font-size: 0.9rem; }
.spec strong { color: var(--pico-muted-color); }
#device-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(100%, 1fr)); gap: var(--pico-grid-column-gap); }
#filter-container { display: flex; flex-wrap: wrap; gap: 1rem; }
#filter-container label { display: flex; flex-direction: column; }
#sort-container { display: flex; gap: 1rem; align-items: end; margin-bottom: 1rem; }

[data-attribute="ram"], [data-attribute="cpu"] {
text-transform: uppercase;
}

</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="base.css">
</head>

<body>
<main class="container">
<h1>Small Phones (<150 mm) 2025</h1>
<p>A curated list of small smartphones with a height under 150 mm. Use the filters and sorting options below to find the perfect compact device for you.</p>
<p><i>Data sourced from GSM Arena and manufacturer specifications.</i></p>
<p>If you find any missing or incorrect data, please open an issue or a pull request on <a href="https://github.com/smaudd/smallphones/issues/">GitHub</a>.</p>
<p>If you want to acces to this data in a RAW format, you can download the <a href="data.csv" download>CSV file here</a>.</p>

<section id="filters">
<h4>Filters</h4>
<div id="filter-container"></div>
</section>

<section id="sorting">
<h4>Sorting</h4>
<div id="sort-container">
<label>
Sort by:
<select id="sort-column">
</select>
</label>
<label>
Order:
<select id="sort-order">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</label>
</div>
</section>

<section id="devices">
<h3>Devices</h3>
<div id="device-list"></div>
</section>
<p>A curated list of small smartphones with a height under 150 mm. Use the filters and sorting options below to
find the perfect compact device for you.</p>
<p><i>Data sourced from GSM Arena and manufacturer specifications.</i></p>
<p>If you find any missing or incorrect data, please open an issue or a pull request on <a
href="https://github.com/smaudd/smallphones/issues/">GitHub</a>.</p>
<p>If you want to access this data in a RAW format, you can <a href="data.csv" download the download>CSV file
here</a>.</p>

<section id="filters">
<h4>Filters</h4>
<div id="filter-container">
<div id="filter-dropdown-container"></div>
<div id="filter-list-container"></div>
</div>
</section>

<section id="sorting">
<h4>Sorting</h4>
<div id="sort-container">
<label>
Sort by:
<select id="sort-column">
</select>
</label>
<label>
Order:
<select id="sort-order">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</label>
</div>
</section>

<section id="devices">
<h3>Devices</h3>
<div id="device-list"></div>
</section>
</main>

<script>
const app = {
data: [],
filters: {},
filterConfig: [
{ key: "brand", label: "Brand" },
{ key: "launch_year", label: "Launch Year" },
{ key: "operating_system", label: "Operating System" },
{ key: "cpu", label: "CPU" }
],
sortColumn: 'height',
sortOrder: 'asc',
maxHeight: 0,
maxWidth: 0,

async init() {
await this.loadData();
this.setupFilters();
this.setupSorting();
this.renderDevices();
},

async loadData() {
const response = await fetch("data.csv");
const text = await response.text();
const rows = text.trim().split('\n');
const headers = rows[0].split(',');
this.data = rows.slice(1).map(row => {
const cols = row.split(',');
const obj = {};
headers.forEach((h, i) => obj[h] = isNaN(+cols[i]) ? cols[i] : +cols[i]);
return obj;
});
this.maxHeight = Math.max(...this.data.map(d => d.height));
this.maxWidth = Math.max(...this.data.map(d => d.width));
},

setupFilters() {
const filterContainer = document.getElementById("filter-container");
this.filterConfig.forEach(config => {
this.filters[config.key] = "all";
const select = document.createElement("select");
select.id = `${config.key}-select`;

const uniqueValues = ["all", ...new Set(this.data.map(d => d[config.key]))];
uniqueValues.forEach(value => {
const option = document.createElement("option");
option.value = value;
option.textContent = value === "all" ? `All ${config.label}` : value;
select.appendChild(option);
});

select.addEventListener("change", () => {
this.filters[config.key] = select.value;
this.renderDevices();
});

const label = document.createElement("label");
label.textContent = `${config.label}: `;
label.appendChild(select);
filterContainer.appendChild(label);
});
},

setupSorting() {
const sortColumnSelect = document.getElementById("sort-column");
const headers = Object.keys(this.data[0]);
headers.forEach(header => {
const option = document.createElement("option");
option.value = header;
option.textContent = header.charAt(0).toUpperCase() + header.slice(1).replace('_', ' ');
if (header === this.sortColumn) option.selected = true;
sortColumnSelect.appendChild(option);
});

sortColumnSelect.addEventListener("change", () => {
this.sortColumn = sortColumnSelect.value;
this.renderDevices();
});

const sortOrderSelect = document.getElementById("sort-order");
sortOrderSelect.value = this.sortOrder;
sortOrderSelect.addEventListener("change", () => {
this.sortOrder = sortOrderSelect.value;
this.renderDevices();
});
},

renderDevices() {
let filteredData = this.data.filter(d => {
return Object.entries(this.filters).every(([key, value]) => {
return value === "all" || d[key] == value;
});
});

filteredData.sort((a, b) => {
const aVal = a[this.sortColumn];
const bVal = b[this.sortColumn];
if (typeof aVal === 'number' && typeof bVal === 'number') {
return this.sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
}
return this.sortOrder === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});

const deviceList = document.getElementById("device-list");
deviceList.innerHTML = "";
filteredData.forEach(d => {
const card = document.createElement("article");
card.className = "device-card";

const scale = 200 / this.maxHeight;
const visualHeight = d.height * scale;
const visualWidth = d.width * scale;

const specs = Object.entries(d).filter(([key]) => key !== 'height' && key !== 'width').map(([key, value]) => {
let unit = '';
let val = value;
let parsedKey = key.trim()
if (['depth'].includes(parsedKey)) unit = ' mm';
else if (parsedKey === 'screen_size') unit = ' inch';
else if (parsedKey === 'ram') unit = ' GB';
else if (parsedKey === 'battery') unit = ' mAh';
return `<div class="spec"><strong data-attribute="${key}">${parsedKey.charAt(0).toUpperCase() + parsedKey.slice(1).replace('_', ' ')}:</strong> ${value}${unit}</div>`;
}).join('');

card.innerHTML = `
<h3>${d.brand} ${d.model}</h3>
<div class="dimensions">
<div class="dimension-box" style="width: ${visualWidth}px; height: ${visualHeight}px;">${d.screen_size}\"\</div>
<p><strong>Height:</strong> ${d.height}mm<br><strong>Width:</strong> ${d.width}mm</p>
</div>
<div class="specs">${specs}</div>
`;
deviceList.appendChild(card);
});
}
};

app.init();
</script>
<script src="index.js"></script>
</body>
</html>

</html>
Loading