PC configure

by PhilQ

HTML

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
<div id="app">

	<div
		class="part"
		v-for="(part, part_i, part_idx) in parts" :key="part_i"
		:class="{'folded': folded[part_i]}"
	>
		<h2 @click="toggle(part_i)">{{ (part_idx+1) }}. {{ part_i }}<span class="selected-part" v-if="folded[part_i]">{{ parts[part_i][ selection[part_i].item ].name }} | {{ euroFormatted(sorted(parts[part_i][ selection[part_i].item ].prices, 'price')[ selection[part_i].price ].price) }}</span></h2>
		<table>
			<tr>
				<td class="select"></td>
				<td class="name">Name</td>
				<td v-for="(attr, attr_i, attr_idx) in part[0].attributes" :key="attr_i">{{ attr_i }}</td>
			</tr>
			<tr
				v-for="(item, item_i) in sorted(part, 'name')" :key="item_i"
				@click="selectPart(part_i, item_i)"
				:class="{'selected': selection[part_i].item===item_i}"
			>
				<td class="select"><input type="radio" :value="item_i" v-model="selection[part_i].item" /></td>
				<td class="name">{{ item.name }}</td>
				<td class="prices">
					<select
						v-model="item.selected_price"
						@click.stop
						@change="selectPrice(part_i, item_i)"
					>
						<option
							v-for="(price, price_i) in sorted(item.prices, 'price')" :key="price_i"
							:value="price_i"
							:disabled="!price.available"
						>{{ euroFormatted(price.price) }} @ {{ price.shop }}</option>
					</select>
				</td>
				<td v-for="(attr, attr_i) in item.attributes" :key="attr_i">{{ attr }}</td>
			</tr>
		</table>
	</div>
	
	<div class="part">
		<h2>Total<span class="selected-part">{{ euroFormatted(total) }}</span></h2>
	</div>

</div>

SCSS

@mixin no-selection {
	user-select: none; /* standard syntax */
	-webkit-user-select: none; /* webkit (safari, chrome) browsers */
	-moz-user-select: none; /* mozilla browsers */
	-khtml-user-select: none; /* webkit (konqueror) browsers */
	-ms-user-select: none; /* IE10+ */
}

*, :before, :after { margin: 0; padding: 0; box-sizing: border-box; }

body {
  background: #20262E;
  font-family: Helvetica;
}

#app {
  padding: 0 20px;
}

.part {
	margin-top: 20px;
  background: #fff;
  border-radius: 4px;
  padding: 20px;
  transition: all 0.2s;

	h2 {
		font-weight: bold;
		text-transform: uppercase;
		@include no-selection;
		
		.selected-part {
			float: right;
			text-transform: none;
			opacity: 60%;
		}
	}
	
	table {
		margin-top: 10px;
		width: 100%;
		font-size: 0.85rem;

		tr:first-child td {
			font-size: 60%;
			text-transform: uppercase;
			color: #888;
			@include no-selection;
		}

		.select {
			width: 30px;
			text-align: left;
		}
		
		.name {
		}
		
		tr.selected td {
			font-weight: bold;
		}
	}
	
	&.folded {
		table {
			display: none;
		}
	}
}

Vue

new Vue({
	el: "#app",
	data: {
		folded: {
			cpu: false,
			// ...
		},
		selection: {
			cpu: { item: 0, price: 0 }, // itemIdx, priceIdx
			// ...
		},
		parts: {
			cpu: [
				{
					name: 'CPU Item 1',
					attributes: {},
					prices: [
						{ shop: 'None', price: 10.00, available: true },
						{ shop: 'Shopname', price: 0.00, available: true },
					],
					selected_price: 0,
				},
				{
					name: 'CPU Item 2',
					attributes: {},
					prices: [
						{ shop: 'None', price: 0.00, available: true },
						{ shop: 'Shopname', price: 10.00, available: true },
					],
					selected_price: 0,
				},
			],
			// ...
		},
	},
	computed: {
		total: function() {
			let total = 0;
			for (const k in this.selection) {
				let currentPart = this.parts[ k ][ this.selection[ k ].item ];
				let priceIdx = currentPart.selected_price;
				total += this.sorted(currentPart.prices, 'price')[ priceIdx ].price;
			}
			return total;
		},
	},
	mounted() {
		for (const k in this.selection) {
			this.parts[ k ][ this.selection[ k ].item ].selected_price = this.selection[ k ].price;
		}
	},
	methods: {
		dotGet: function(t, path) {
			return path.split(".").reduce((r, k) => r?.[k], t);
		},
		sorted: function(items, orderBy = '', orderDirection = 'asc') {
			return items
				.slice()
				.sort((a, b) => {
					orderBy = orderBy.length ? orderBy : 'name';
					let direction = (orderDirection === 'desc' ? -1 : 1);
					let _a = this.dotGet(a, orderBy);
					let _b = this.dotGet(b, orderBy);
					if (
						typeof _a === 'undefined'
						|| typeof _b === 'undefined'
					) {
						return 0;
					}
					let valA = (typeof _a === 'number' ? _a : _a.toUpperCase());
					let valB = (typeof _b === 'number' ? _b : _b.toUpperCase());
					if (valA < valB) {
						return -1 * direction;
					}
					if (valA > valB) {
						return 1 * direction;
					}
					return 0;
				});
		},
		euroFormatted:...