pricecharts

track prices of consumer electronics
Log | Files | Refs | README

commit b492c79b0aa52d1f51cdfc4bcd611ec32e2fc00d
parent 6a4e986e8c8277956e02505541c18d8cb2139ae1
Author: kyle <kyle@getaddrinfo.net>
Date:   Sun,  4 Oct 2015 23:07:56 -0600

ps_html: start website re-work

- merge manufacturers, retailers, and product type listing into single page
- add product type logos
- speed up db operations by not doing table joins
- speed up db operations by not using db to find extreme values
- polish about page text, add statistics
- start generalizing svg generation code for other chart types

Diffstat:
MDEPS | 1+
MMakefile | 2+-
MPriceSloth.pm | 5++++-
Acharts.css | 41+++++++++++++++++++++++++++++++++++++++++
Alogo/hard_drive.jpg | 0
Alogo/laptop.jpg | 0
Alogo/memory.jpg | 0
Rpricesloth.jpg -> logo/pricesloth.jpg | 0
Alogo/processor.png | 0
Alogo/television.jpg | 0
Alogo/video_card.jpg | 0
Mpricesloth.css | 140+++++++++++++++++++++++++++++++++++--------------------------------------------
Mps_html | 249+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Att/about.tt | 39+++++++++++++++++++++++++++++++++++++++
Dtt/coarse_list.tt | 42------------------------------------------
Mtt/fine_list.tt | 2+-
Mtt/index.tt | 114+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mtt/wrapper.tt | 25+++++++++++++------------
18 files changed, 390 insertions(+), 270 deletions(-)

diff --git a/DEPS b/DEPS @@ -1,3 +1,4 @@ +p5-BSD-arc4random p5-Config-Grammar p5-DBD-SQLite p5-Email-Send diff --git a/Makefile b/Makefile @@ -12,7 +12,7 @@ DEV_BIN = /home/kyle/src/pricesloth BINS = ps_html ps_fcgi price_scraper product_scraper # WARNING stupid idiom used below if adding > 1 item to LIBS!! LIBS = PriceSloth.pm -HTML = tt logo pricesloth.css pricesloth.jpg +HTML = tt logo pricesloth.css charts.css install: cp $(BINS) $(USR_BIN)/ diff --git a/PriceSloth.pm b/PriceSloth.pm @@ -47,6 +47,9 @@ sub get_config }); my $cfg_file = "/etc/pricesloth.cfg"; + if (-e "pricesloth.cfg") { + $cfg_file = "pricesloth.cfg"; + } my $cfg = $parser->parse($cfg_file) or die "error: $parser->{err}\n"; return $cfg; @@ -58,7 +61,6 @@ sub get_dbh my $verbose = shift || undef; make_path($db_dir, { verbose => $verbose }); - print "info: get_dbh: opening $db_dir/db\n" if ($verbose); my $dbh = DBI->connect( "dbi:SQLite:dbname=$db_dir/db", "", @@ -66,6 +68,7 @@ sub get_dbh { RaiseError => 1 } ) or die $DBI::errstr; + print "info: opened $db_dir/db\n" if ($verbose); return $dbh; } diff --git a/charts.css b/charts.css @@ -0,0 +1,41 @@ +/* horizontal rulers, plus date ticks */ +.chart_rulers { + stroke: #BBB; + stroke-width: 0.5; +} + +/* x axis labels */ +.chart_date { + font-size: 0.6em; + font-family: sans-serif; + fill: black; +} + +/* y axis labels */ +.chart_price { + font-size: 0.7em; + font-family: sans-serif; + fill: black; +} + +/* retailer date series */ +.chart_series { + fill-opacity: 0; + stroke-width: 2; + stroke-opacity: 0.8; +} + +.chart_series:hover { + stroke-width: 3; +} + +.chart_retailer_text { + font-size: 0.6em; + font-family: sans-serif; +} + +.chart_data { + fill: white; + stroke-width: 2; + stroke-opacity: 0.8; +} diff --git a/logo/hard_drive.jpg b/logo/hard_drive.jpg Binary files differ. diff --git a/logo/laptop.jpg b/logo/laptop.jpg Binary files differ. diff --git a/logo/memory.jpg b/logo/memory.jpg Binary files differ. diff --git a/pricesloth.jpg b/logo/pricesloth.jpg Binary files differ. diff --git a/logo/processor.png b/logo/processor.png Binary files differ. diff --git a/logo/television.jpg b/logo/television.jpg Binary files differ. diff --git a/logo/video_card.jpg b/logo/video_card.jpg Binary files differ. diff --git a/pricesloth.css b/pricesloth.css @@ -6,50 +6,86 @@ p { line-height: 1.5em; } +li { + line-height: 1.5em; +} + +body { + /* margin-top: 1em; */ + font-family: sans-serif; + + /* i don't want to read anything wider than that */ + max-width: 80em; + margin: auto; + + border-left: 1px dashed gray; + border-right: 1px dashed gray; +} + +.menu_bar { + /* space at the top for round sloth face to poke out */ + margin-top: 1.25em; + background-color: green; +} + +.menu_bar_title { + font-size: 2em; + /* width of the sloth face... ish */ + margin-left: 3em; + + display: inline-block; + color: white; + font-weight: bold; +} + +.search_box { + display: inline-block; + height: 2em; +} + .column { display: inline-block; vertical-align: top; -} -.index_column { - width: 40%; - padding: 0 4% 0 4%; + /* by default there will be two of these side by side */ + width: 45.5%; + padding: 0 2% 0 2%; } .product { padding: 0.5em; - margin: 0.3em 0 0.3em 0; + margin: 0.3em 0.3em 0.3em 0.3em; border: 1px solid gray; border-radius: 3px; - min-width: 800px; } .sloth { - height: 3em; -} + width: 5em; + height: 5em; + border-radius: 2.5em; + -webkit-border-radius: 2.5em; + -moz-border-radius: 2.5em; + background: url(http://pricesloth.com/pricesloth.jpg) no-repeat; -.manufacturers { - padding: 0 2% 0 2%; - width: 29%; + margin-top: -1.25em; + + margin-left: 0.5em; + position: fixed; } @media (max-width: 640px) { - .manufacturers { - width: 100%; - padding: 0 0 0 0; + /* anything smaller than this go into single user mode */ + .column { + width: 90%; + padding: 0 5% 0 5%; } - .index_column { - width: 100%; - padding: 0 0 0 0; + .logo { + max-width: 100%; } } @media (max-width: 1280) { - .manufacturers { - width: 40%; - padding: 0 5% 0 5%; - } } @media print { @@ -59,21 +95,15 @@ p { } .logo { - vertical-align: middle; - padding: 5px; -} - -.manufacturers img { height: 3em; } -.retailers { - padding: 0 2% 0 2%; - width: 95%%; +.retailer_logo { + width: 15em; } -.retailers img { - height: 4em; +.product_type { + width: 15em; } .product_index { @@ -84,54 +114,8 @@ p { } .list_item { - padding: 5px; } .logo_small { - height: 2em; - vertical-align: middle; -} - -/* horizontal rulers, plus date ticks */ -.chart_rulers { - stroke: #BBB; - stroke-width: 0.5; -} - -/* x axis labels */ -.chart_date { - font-size: 0.6em; - fill: black; -} - -/* y axis labels */ -.chart_price { - font-size: 0.7em; - fill: black; -} - -/* retailer date series */ -.chart_series { - fill-opacity: 0; - stroke-width: 2; - stroke-opacity: 0.8; -} - -.chart_series:hover { - stroke-width: 3; -} - -.chart_retailer_text { - font-size: 0.6em; - font-family: sans-serif; -} - -.chart_data { - fill: white; - stroke-width: 2; - stroke-opacity: 0.8; -} - -.breakdown { - font-size: 0.8em; + height: 0.7em; } diff --git a/ps_html b/ps_html @@ -3,7 +3,9 @@ use strict; use warnings; +use BSD::arc4random qw(:all); use Config::Grammar; +use Data::Dumper; use Getopt::Std; use Lingua::EN::Inflect qw(PL); use Math::MatrixReal; @@ -23,7 +25,7 @@ my $dbh = get_dbh($cfg->{general}{db_dir}, $args{v}); my $work_dir = $cfg->{http}{htdocs}; my $svg_dir = $work_dir . "/svg"; -print "info: work, svg dirs $work_dir\{,svg\}\n" if ($args{v}); +print "info: work, svg dirs $work_dir/\{,svg\}\n" if ($args{v}); my $config = { POST_CHOMP => 1, EVAL_PERL => 1, @@ -33,98 +35,162 @@ my $www = Template->new($config) || die Template->error(), "\n"; my $and_stale = $args{a} ? "" : "and products.svg_stale = 1"; my $where_stale = $args{a} ? "" : "where svg_stale = 1"; -my $part_equality = qq{prices.manufacturer = products.manufacturer and - prices.part_num = products.part_num}; my $desc_sth = $dbh->prepare(qq{select description from descriptions where manufacturer = ? and part_num = ? order by date}); # catmull-rom to cubic bezier conversion matrix my $catrom_to_bezier = Math::MatrixReal->new_from_rows( - [[0, 1, 0, 0], + [[0, 1, 0, 0], [-1/6, 1, 1/6, 0], [0, 1/6, 1, -1/6], [0, 0, 1, 0]] ); my $m_t = ~$catrom_to_bezier; + # # manufacturers # -my $stale_list = qq{select distinct prices.manufacturer from prices, products where - $part_equality $and_stale}; +my $stale_list = qq{select distinct manufacturer from products $where_stale}; -my $types = qq{select distinct products.type from prices, products where - $part_equality and prices.manufacturer = ? $and_stale}; +my $types = qq{select distinct type from products where manufacturer = ? $and_stale}; -my $products_fine = qq{select distinct products.manufacturer, products.part_num - from products, prices where $part_equality and products.type = ? and - products.manufacturer = ?}; +my $products_fine = qq{select distinct manufacturer, part_num + from products where type = ? and manufacturer = ?}; my $summary = qq{select type, count(*) from products where manufacturer = ? group by type}; -my $coarse_sql = qq{select products.manufacturer, count(distinct products.part_num) - as count, products.type from products, prices where $part_equality - group by products.manufacturer, products.type}; +generate_folder($stale_list, $types, $products_fine, "Manufacturers", $summary); + +# most natural grouping is manufacturer then type +# (answers the question: what types of products does this manufacturer make?) +my $coarse_sql = qq{select manufacturer, count(distinct part_num) as count, + type from products group by manufacturer, type}; my @key_fields = ("manufacturer", "type"); -generate_folder($stale_list, $types, $products_fine, "Manufacturers", $summary, - $coarse_sql, \@key_fields); +# second most natural grouping is manufacturer then retailer +# (answers the question: which places sell this manufacturer?) + +my $manufacturer_list = $dbh->selectall_hashref($coarse_sql, \@key_fields); +# print Dumper($manufacturer_list); + # # retailers # -$stale_list = qq{select distinct prices.retailer from prices, products where - $part_equality $and_stale}; +$stale_list = qq{select distinct retailer from prices $where_stale}; -$types = qq{select distinct products.type from prices, products where - $part_equality and prices.retailer = ? $and_stale}; +$types = qq{select distinct manufacturer from prices where retailer = ? $and_stale}; -$products_fine = qq{select distinct prices.manufacturer, prices.part_num - from prices, products where $part_equality and products.type = ? and - prices.retailer = ?}; +$products_fine = qq{select distinct manufacturer, part_num + from prices where manufacturer = ? and retailer = ?}; $summary = qq{select manufacturer, count(*) from prices where retailer = ? group by manufacturer}; -$coarse_sql = qq{select prices.retailer, count(distinct products.part_num) as - count, products.type from products, prices where $part_equality group by - prices.retailer, products.type}; -@key_fields = ("retailer", "type"); +generate_folder($stale_list, $types, $products_fine, "Retailers", $summary); -generate_folder($stale_list, $types, $products_fine, "Retailers", $summary, - $coarse_sql, \@key_fields); +# most natural grouping here is a toss up between type and manufacturer +# (answers the question: what manufacturers does this retailer sell?) +$coarse_sql = qq{select retailer, count(distinct part_num) as count, + manufacturer from prices group by retailer, manufacturer}; +@key_fields = ("retailer", "manufacturer"); + +# second grouping is retailer then type +# (answers the question: what types of products does this retailer sell?) + +my $retailer_list = $dbh->selectall_hashref($coarse_sql, \@key_fields); +# print Dumper($retailer_list); # # product types # -$stale_list = qq{select distinct products.type from products, prices where - $part_equality $and_stale}; +$stale_list = qq{select distinct type from products $where_stale}; -$types = qq{select distinct products.manufacturer from prices, products where - $part_equality and products.type = ?}; +$types = qq{select distinct manufacturer from products where type = ?}; -$products_fine = qq{select distinct prices.manufacturer, prices.part_num - from prices, products where $part_equality and products.manufacturer = ? - and products.type = ?}; +$products_fine = qq{select distinct manufacturer, part_num from products where + manufacturer = ? and type = ?}; $summary = qq{select manufacturer, count(*) from products where type = ? group by manufacturer}; -$coarse_sql = qq{select products.type, count(distinct products.part_num) as count, - products.manufacturer from products, prices where $part_equality - group by products.type, products.manufacturer}; +generate_folder($stale_list, $types, $products_fine, "Types", $summary); + +$coarse_sql = qq{select type, count(distinct part_num) as count, + manufacturer from products group by type, manufacturer}; @key_fields = ("type", "manufacturer"); -generate_folder($stale_list, $types, $products_fine, "Types", $summary, - $coarse_sql, \@key_fields); +my $types_list = $dbh->selectall_hashref($coarse_sql, \@key_fields); +# print Dumper($types_list); + +# +# index +# +my $vars = { + manufacturer_list => $manufacturer_list, + retailer_list => $retailer_list, + types_list => $types_list +}; +$www->process("index.tt", $vars, "index.html") + or die $www->error(), "\n"; + +# +# about +# +my ($p, $m) = $dbh->selectrow_array(qq{select count(*), + count(distinct manufacturer) from products}); + +# anything we haven't seen for over 30 days is stale +my ($prod_stale) = $dbh->selectrow_array(qq{select count(*) from products + where last_seen < ?}, undef, time - (30 * 24 * 60 * 60)); + +my ($r) = $dbh->selectrow_array("select count(*) from retailers"); + +# XXX: this isn't right, it wont count parts with equal part numbers but +# different manufacturers +my ($have_prices) = $dbh->selectrow_array(qq{select count(distinct part_num) + from prices}); + +# draw a graph of total number of products vs time +my ($first_seen) = $dbh->selectrow_array("select first_seen from products order by first_seen limit 1"); +my $num_weeks = (time - $first_seen) / (60 * 60 * 24 * 7); +my %totals_series; +for my $i (0..$num_weeks) { + my $x = $first_seen + $i * (60 * 60 * 24 * 7); + my ($y) = $dbh->selectrow_array("select count(*) from products where first_seen < ?", undef, $x); + $totals_series{"Total"}{$x} = { "price" => $y }; + + ($y) = $dbh->selectrow_array("select count(*) from products where last_seen < ?", undef, $x - (60 * 60 * 24 * 30)); + $totals_series{"Stale"}{$x} = { "price" => $y }; +} + +# print Dumper(%totals_series); +my %series_metadata; +$series_metadata{"Total"} = { url => "", color => "000" }; +$series_metadata{"Stale"} = { url => "", color => "F00" }; +my $svg = make_svg(\%totals_series, "no_part_num", \%series_metadata, 1, ""); + +my $svg_path = "$svg_dir/history_summary.svg"; +open my $svg_fh, ">", "$svg_path" or die "couldn't open $svg_path: $!"; +print $svg_fh $svg->xmlify; +close $svg_fh; + +$vars = { nret => $r, nmanuf => $m, nprod => $p - $prod_stale, + nprod_stale => $prod_stale, no_prices => $p - $have_prices +}; +$www->process("about.tt", $vars, "about.html") or die $www->error(), "\n"; + +print "info: about.html done\n" if ($args{v}); + # # products # print "info: products: " if ($args{v}); -my $sql = "select * from products, prices where $part_equality $and_stale"; +my $sql = "select * from products $where_stale"; my $products = $dbh->selectall_hashref($sql, "part_num"); while (my ($part_num, $row) = each %$products) { my $part_link = linkify($part_num); @@ -138,29 +204,9 @@ while (my ($part_num, $row) = each %$products) { } print scalar(keys %$products) . " processed\n" if ($args{v}); -# -# index -# -print "info: index: " if ($args{v}); - -my $new = $dbh->selectall_arrayref(qq{select manufacturer, part_num - from products order by first_seen desc limit 5}); - -my $upd = $dbh->selectall_arrayref(qq{select manufacturer, part_num - from products order by last_scraped desc limit 5}); - -my ($p, $m) = $dbh->selectrow_array(qq{select count(*), count(distinct manufacturer) - from products where last_seen > ?}, undef, time - (30 * 24 * 60 * 60)); - -my ($r) = $dbh->selectrow_array("select count(*) from retailers"); - -my $vars = { nret => $r, nmanuf => $m, nprod => $p, news => $new, upds => $upd }; -$www->process("index.tt", $vars, "index.html") or die $www->error(), "\n"; - -print "$p products, $m manufacturers, $r retailers\n" if ($args{v}); # -# svg +# product svg;s # print "info: svg: " if ($args{v}); @@ -168,25 +214,20 @@ my @series_keys = ("retailer", "date"); my $series_sth = $dbh->prepare(qq{select retailer, date, price from prices where manufacturer = ? and part_num = ?}); -my $extremes_sth = $dbh->prepare(qq{select min(date), max(date), min(price), - max(price) from prices where manufacturer = ? and part_num = ?}); - my $retailer_info = $dbh->selectall_hashref(qq{select name, url, color from retailers}, "name"); -my $parts_sth = $dbh->prepare(qq{select distinct products.manufacturer, - products.part_num from products, prices where $part_equality $and_stale}); +my $parts_sth = $dbh->prepare(qq{select distinct manufacturer, + part_num from prices $where_stale}); $parts_sth->execute(); my $rendered = 0; while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) { spin() if ($args{v}); - my ($x_min, $x_max, $y_min, $y_max) = - $dbh->selectrow_array($extremes_sth, undef, $manufacturer, $part_num); my $series = $dbh->selectall_hashref($series_sth, \@series_keys, undef, $manufacturer, $part_num); - my $svg = make_svg($series, $x_min, $x_max, $y_min, $y_max, $part_num); + my $svg = make_svg($series, $part_num, $retailer_info); my $manufacturer_dir = linkify($manufacturer); my $part_link = linkify($part_num); @@ -215,19 +256,17 @@ sub generate_folder my $sql_products = shift; my $name = shift; my $sql_summary = shift; - my $coarse_sql = shift; - my $coarse_keys_ref = shift; my $name_lc = lc ($name); - print "info: $name_lc: " if ($args{v}); + print "info: $name_lc/*: " if ($args{v}); my $stale_list = $dbh->selectcol_arrayref($sql_stale_outer); for my $it (@$stale_list) { - spin() if ($args{v}); my $it_link = linkify($it); my $types = $dbh->selectcol_arrayref($sql_types, undef, $it); for my $type (sort @$types) { + spin() if ($args{v}); my $products = $dbh->selectall_arrayref($sql_products, undef, $type, $it); $_->[2] = get_description($_->[0], $_->[1]) for (@$products); @@ -248,13 +287,6 @@ sub generate_folder or die $www->error(), "\n"; } print "\b" . scalar @$stale_list . " processed\n" if ($args{v}); - - return unless (@$stale_list); - - my $coarse_list = $dbh->selectall_hashref($coarse_sql, $coarse_keys_ref); - my $vars = { name => $name, list => $coarse_list }; - $www->process("coarse_list.tt", $vars, "$name_lc.html") - or die $www->error(), "\n"; } sub linkify @@ -293,18 +325,36 @@ sub get_description sub make_svg { my $series = shift; - my $x_min = shift; - my $x_max = shift; - my $y_min = shift; - my $y_max = shift; my $part_num = shift; + my $metadata = shift; + my $short = shift || 0; + my $right_axis_prefix = "\$"; my ($left, $center, $right, $top, $middle, $bottom) = (3, 957, 40, 15, 150, 20); + if ($short) { + $center = 457; + $right_axis_prefix = ""; + } + my $width = $right + $center + $left; my $height = $top + $middle + $bottom; - $y_max = ceil($y_max / 100) * 100; - $y_min = floor($y_min / 100) * 100; + my ($x_min, $x_max, $y_min, $y_max) = (100000000000, 0, 1000000, 0); + while (my ($retailer, $values) = each %$series) { + for (keys %{$values}) { + my ($x, $y) = ($_, $values->{$_}{price}); + $x_min = $x if ($x < $x_min); + $x_max = $x if ($x > $x_max); + $y_min = $y if ($y < $y_min); + $y_max = $y if ($y > $y_max); + } + } + + my $num_digits = ceil(log($y_max) / log(10)); + my $magnitude = 10 ** ($num_digits - 1); + + $y_max = ceil($y_max / $magnitude) * $magnitude; + $y_min = floor($y_min / $magnitude) * $magnitude; my ($domain, $range) = ($x_max - $x_min, $y_max - $y_min); $domain = 24 * 60 * 60 if ($domain <= 0); @@ -315,7 +365,7 @@ sub make_svg my $defs = $svg->defs(); my ($x_scale, $y_scale) = ($center / $domain, $middle / $range); - $defs->tag("link", href => "/pricesloth.css", type => "text/css", + $defs->tag("link", href => "/charts.css", type => "text/css", rel => "stylesheet", xmlns => "http://www.w3.org/1999/xhtml"); # make price labels along right side and lines across chart @@ -328,7 +378,7 @@ sub make_svg $svg->text( id => "price_$_", x => $left + $center + 5, y => $y + 3, class => "chart_price", "text-anchor" => "start" - )->cdata("\$$price"); + )->cdata("$right_axis_prefix$price"); $svg->line( id => "horizontal_line_$_", x1 => $left, y1 => $y, @@ -336,6 +386,11 @@ sub make_svg ); } + $num_labels = 8; + if ($short) { + $num_labels = 5; + } + # make dates along the bottom if ($domain == 24 * 60 * 60) { $num_labels = 2; @@ -353,7 +408,7 @@ sub make_svg my $time = $x_min + $domain * $step; $svg->text(id => "date_$_", x => $x, y => $height - 5, class => "chart_date", "text-anchor" => $text_anchor - )->cdata(strftime("%B %e, %Y", localtime($time))); + )->cdata(strftime("%b %e, %Y", localtime($time))); # print the little tick marks down from the x axis my $x_axis = $top + $middle; @@ -383,7 +438,7 @@ sub make_svg $defs->tag("path", "d" => $d, id => "path_$retailer_id"); } - my $info = $retailer_info->{$retailer}; + my $info = $metadata->{$retailer}; my ($url, $color) = ($info->{url}, $info->{color}); # xlink:href's don't like raw ampersands @@ -399,11 +454,12 @@ sub make_svg ); # now draw individual data points - $defs->circle(id => "data_point_$retailer", cx => 0, cy => 0, + my $rand_token = sprintf("%x", arc4random()); + $defs->circle(id => $rand_token, cx => 0, cy => 0, class => "chart_data", r => 2, style => "stroke: #$color;" ); while (my $i = each @xs) { - $anchor->use(-href => "#data_point_$retailer", + $anchor->use(-href => "#$rand_token", x => $xs[$i], y => $ys[$i] ); } @@ -459,7 +515,10 @@ sub catmullrom_to_bezier $y_row = $y_row * $m_t; my ($x, $y) = ($x_row->[0][0], $y_row->[0][0]); - $d .= "C $x->[1], $y->[1] $x->[2], $y->[2] $x->[3], $y->[3] "; + + # knock some digits of precision off + $d .= sprintf("C %0.2f, %0.2f %0.2f, %0.2f %0.2f, %0.2f ", + $x->[1], $y->[1], $x->[2], $y->[2], $x->[3], $y->[3]); } return $d; diff --git a/tt/about.tt b/tt/about.tt @@ -0,0 +1,39 @@ +[% WRAPPER wrapper.tt %] + <div class="column"> + <h2>About</h2> + + <p>Hi, and welcome to <b>PriceSloth</b>, a purchasing aid for + consumer electronics. + This site contains price charts that let you pick the best + retailer to purchase a particular product from, and help you + predict what the future price of a product might be. + </p> + + <p>The price charts provide two main things: + <ul> + <li>Inter-retailer price comparisons + <li>Historical pricing information + </ul> + </p> + <p>Inter-retailer price comparison saves you money by showing you + the retailer that is offering a certain product for the lowest + cost. It shows you which retailers offer that product, too. + </p> + <p>Historical pricing information can show periods when the + product was on sale, and the overall trend of the price since + the product came onto the market. This makes picking the + absolute best time to buy a product easier.</p> + </div> + + <div class="column index_column"> + <h2>Status</h2> + <p>Online. Currently tracking <b>[% nprod %]</b> products, + <b>[% nmanuf %]</b> manufacturers, and + <b>[% nret %]</b> retailers. There are <b>[% nprod_stale %]</b> + stale products and <b>[% no_prices %]</b> products don't have a + single price.</p> + + <object data="/svg/history_summary.svg" type="image/svg+xml"> + </object> + </div> +[% END %] diff --git a/tt/coarse_list.tt b/tt/coarse_list.tt @@ -1,42 +0,0 @@ -[% WRAPPER wrapper.tt %] -[% USE POSIX %] -[% boundary = POSIX.ceil(list.keys.size / 3.0) %] - - <h1>[% name %] ([% list.keys.size %])</h1> - [% name_link = name.lower.replace('[ #\/]', '_') %] - - <div class="column [% name_link %]"> - <ul> - [% i = 0 %] - [% FOREACH item IN list.keys.sort %] - [% item_link = item.lower.replace('[ #\/]', '_') %] - [% PERL %] - my $link = $stash->get("item_link"); - my ($logo) = glob("logo/$link.*"); - $stash->set("logo", $logo); - [% END %] - [% IF i != 0 && (i % boundary) == 0 %] - </ul> - </div> - - <div class="column [% name_link %]"> - <ul> - [% END %] - [% i = i + 1 %] - <li><div class="list_item"> - <a href="/[% name_link %]/[% item_link %].html"> - <img alt="[% item %]" src="/[% logo %]" /> - </a><br> - - <div class="breakdown"> - [% FOREACH pair IN list.$item.pairs %] - [% n = pair.value.count %] - [% type_link = pair.key.lower.replace('[ #\/]', '_') %] - [% n %] <a href="/[% name_link %]/[% item_link %]/[% type_link %].html"> - [% pair.key %]</a>, - [% END %] - </div></div> - [% END %] - </ul> - </div> -[% END %] diff --git a/tt/fine_list.tt b/tt/fine_list.tt @@ -6,7 +6,7 @@ $stash->set("logo", $logo); [% END %] <h1><img alt="[% name %]" class="logo_small" src="/[% logo %]"/> - [% products.size %] [% type %]</h1> + [% name %] [% type %] ([% products.size %] total)</h1> [% FOREACH product IN products %] <div class="product"> diff --git a/tt/index.tt b/tt/index.tt @@ -1,49 +1,83 @@ [% WRAPPER wrapper.tt %] - <div class="column index_column"> - <h1>Welcome</h1> - - <p>Welcome to <b>PriceSloth</b>, a price comparison - tool for consumer electronics. - Currently <b>[% nprod %]</b> products from - <b>[% nmanuf %]</b> - <a href="/manufacturers.html">manufacturers</a> - are tracked across <b>[% nret %]</b> - <a href="/retailers.html">retailers</a>.</p> - - <p>To start, try searching for a product or manufacturer in the - search box at the top.</p> - - <p><b>PriceSloth</b> periodically looks up and saves the prices - of different products and converts this information into - charts.</p> + + <div class="column"> + <h2>Manufacturers ([% manufacturer_list.keys.size %] total)</h2> + + [% FOREACH item IN manufacturer_list.keys.sort %] + [% item_link = item.lower.replace('[ #\/]', '_') %] + [% PERL %] + my $link = $stash->get("item_link"); + # glob onto file with any extension + my ($logo) = glob("logo/$link.*"); + $stash->set("logo", $logo); + [% END %] + <div class="list_item"> + <a href="/manufacturers/[% item_link %].html"> + <img class="logo" alt="[% item %]" src="/[% logo %]" /> + </a><br> + + Types: + [% FOREACH pair IN manufacturer_list.$item.pairs %] + [% n = pair.value.count %] + [% type_link = pair.key.lower.replace('[ #\/]', '_') %] + [% n %] <a href="/manufacturers/[% item_link %]/[% type_link %].html"> + [% pair.key %]</a>, + [% END %] + <br /> + Retailers: XXX, YYY + </div> + <hr /> + [% END %] </div> - <div class="column index_column"> - <h1>New Products ([% news.size %])</h1> - <ul> - [% FOREACH new IN news %] - [% manuf_link = new.0.lower.replace('[ #\/]', '_') %] - [% part_link = new.1.lower.replace('[ #\/]', '_') %] + <div class="column"> + <h2>Retailers ([% retailer_list.keys.size %] total)</h2> + [% FOREACH item IN retailer_list.keys.sort %] + [% item_link = item.lower.replace('[ #\/]', '_') %] + [% PERL %] + my $link = $stash->get("item_link"); + # glob onto file with any extension + my ($logo) = glob("logo/$link.*"); + $stash->set("logo", $logo); + [% END %] + <a href="/retailers/[% item_link %].html"> + <img class="retailer_logo" alt="[% item %]" src="/[% logo %]" /> + </a> + <br /> - <li><img class="logo_small" alt="[% new.0 %]" - src="/logo/[% manuf_link %].svg"></img> + Manufacturers sold here: + [% FOREACH pair IN retailer_list.$item.pairs %] + [% type_link = pair.key.lower.replace('[ #\/]', '_') %] + <a href="/retailers/[% item_link %]/[% type_link %].html"> + [% pair.key %]</a>, + [% END %] + <hr /> + [% END %] - <a href="/products/[% manuf_link %]/[% part_link %].html"> - [% new.1 %]</a> + <h2>Product Types ([% types_list.keys.size %] total)</h2> + [% FOREACH item IN types_list.keys.sort %] + [% item_link = item.lower.replace('[ #\/]', '_') %] + [% PERL %] + my $link = $stash->get("item_link"); + # glob onto file with any extension + my ($logo) = glob("logo/$link.*"); + $stash->set("logo", $logo); [% END %] - </ul> - - <h1>Recently Updated ([% upds.size %])</h1> - <ul> - [% FOREACH upd IN upds %] - [% manuf_link = upd.0.lower.replace('[ #\/]', '_') %] - [% part_link = upd.1.lower.replace('[ #\/]', '_') %] - - <li><img class="logo_small" alt="[% upd.0 %]" - src="/logo/[% manuf_link %].svg"></img> - <a href="/products/[% manuf_link %]/[% part_link %].html"> - [% upd.1 %]</a> + <div class="list_item"> + <a href="/types/[% item_link %].html"> + <img class="product_type" alt="[% item %]" src="/[% logo %]" /> + </a><br /> + + [% item %] manufacturers: + [% FOREACH pair IN types_list.$item.pairs %] + [% type_link = pair.key.lower.replace('[ #\/]', '_') %] + <a href="/types/[% item_link %]/[% type_link %].html"> + [% pair.key %]</a>, [% END %] - </ul> + </div> + + <hr /> + [% END %] </div> + [% END %] diff --git a/tt/wrapper.tt b/tt/wrapper.tt @@ -5,20 +5,21 @@ <meta name="viewport" content="width=device-width" /> <title>PriceSloth</title> <link rel="stylesheet" type="text/css" href="/pricesloth.css" /> - <link rel="icon" type="image/jpeg" href="/pricesloth.jpg"> + <link rel="icon" type="image/jpeg" href="/logo/pricesloth.jpg"> </head> <body> - <div class="menu"> - <a href="/"><img class="sloth" src="/pricesloth.jpg"/></a> - <a href="/manufacturers.html">Manufacturers</a> - <a href="/retailers.html">Retailers</a> - <a href="/types.html">Types</a> - <form method="get" action="/search.html"> - <fieldset> - <input type="text" name="q" /> - <input type="submit" value="Find"> - </fieldset> - </form> + <div class="menu_bar"> + <a href="/about.html"> + <img class="sloth" src="/pricesloth.jpg"/> + </a> + <div class="menu_bar_title">PriceSloth</div> + + <form class="search_box" method="get" action="/search.html"> + <fieldset> + <input type="text" name="q" /> + <input type="submit" value="Search"> + </fieldset> + </form> </div> [% content %]