diff --git a/app.rb b/app.rb index cad78e61..4356295f 100644 --- a/app.rb +++ b/app.rb @@ -82,7 +82,7 @@ def redirect_to_internet_archive_for_geocities_sites end end -WHITELISTED_POST_PATHS = ['/create_validate_all', '/create_validate', '/create'].freeze +WHITELISTED_POST_PATHS = ['/create_validate_all', '/create_validate', '/create', '/search', '/search/'].freeze before do if request.path.match /^\/api\//i diff --git a/app/browse.rb b/app/browse.rb index db94ca21..e8615a73 100644 --- a/app/browse.rb +++ b/app/browse.rb @@ -129,77 +129,3 @@ def browse_sites_dataset ds end - -def daily_search_max? - query_count = $redis_cache.get('search_query_count').to_i - query_count >= $config['google_custom_search_query_limit'] -end - -get '/browse/search' do - @title = 'Site Search' - @description = 'Search websites hosted on Neocities.' - - @daily_search_max_reached = daily_search_max? - - if @daily_search_max_reached - params[:q] = nil - end - - if !params[:q].blank? - created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400) - $redis_cache.incr('search_query_count') unless created - - @start = params[:start].to_i - @start = 0 if @start < 0 - - @resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: { - key: $config['google_custom_search_key'], - cx: $config['google_custom_search_cx'], - safe: 'active', - start: @start, - q: Rack::Utils.escape(params[:q]) + ' -filetype:pdf -filetype:txt site:*.neocities.org' - }) - - @items = [] - - if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0" - @total_results = @resp['searchInformation']['totalResults'].to_i - @resp['items'].each do |item| - link = Addressable::URI.parse(item['link']) - path = link.path || '/' - unencoded_path = begin - Rack::Utils.unescape(Rack::Utils.unescape(path)) # Yes, it needs to be decoded twice - rescue ArgumentError - path # Fall back when the path includes invalid %-encoding - end - item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path - item['link'] = link - - next if link.host == 'neocities.org' - - username = link.host.split('.').first - site = Site[username: username] - next if site.nil? || site.is_deleted || site.is_nsfw - - screenshot_path = unencoded_path - - screenshot_path << 'index' if screenshot_path[-1] == '/' - - ['.html', '.htm'].each do |ext| - if site.screenshot_exists?(screenshot_path + ext, '540x405') - screenshot_path += ext - break - end - end - - item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405') - @items << item - end - end - else - @items = nil - @total_results = 0 - end - - erb :'search' -end diff --git a/app/search.rb b/app/search.rb new file mode 100644 index 00000000..f891b5bb --- /dev/null +++ b/app/search.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +def daily_search_max? + query_count = $redis_cache.get('search_query_count').to_i + query_count >= $config['google_custom_search_query_limit'] +end + +post '/search/?' do + query = params[:q].to_s.strip + redirect query.blank? ? '/search' : "/search?#{Rack::Utils.build_query(q: query)}" +end + +get '/search/?' do + @query = params[:q].to_s.strip + @title = @query.blank? ? 'Neocities Search' : 'Site Search' + @description = 'Search websites hosted on Neocities.' + + @daily_search_max_reached = daily_search_max? + + if @daily_search_max_reached + @items = nil + @total_results = 0 + elsif !@query.blank? + created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400) + $redis_cache.incr('search_query_count') unless created + + @start = params[:start].to_i + @start = 0 if @start < 0 + + @resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: { + key: $config['google_custom_search_key'], + cx: $config['google_custom_search_cx'], + safe: 'active', + start: @start, + q: Rack::Utils.escape(@query) + ' -filetype:pdf -filetype:txt site:*.neocities.org' + }) + + @items = [] + + if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0" + @total_results = @resp['searchInformation']['totalResults'].to_i + @resp['items'].each do |item| + link = Addressable::URI.parse(item['link']) + path = link.path || '/' + unencoded_path = begin + Rack::Utils.unescape(Rack::Utils.unescape(path)) # Yes, it needs to be decoded twice + rescue ArgumentError + path # Fall back when the path includes invalid %-encoding + end + item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path + item['link'] = link + + next if link.host == 'neocities.org' + + username = link.host.split('.').first + site = Site[username: username] + next if site.nil? || site.is_deleted || site.is_nsfw + + screenshot_path = unencoded_path + + screenshot_path << 'index' if screenshot_path[-1] == '/' + + ['.html', '.htm'].each do |ext| + if site.screenshot_exists?(screenshot_path + ext, '540x405') + screenshot_path += ext + break + end + end + + item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405') + @items << item + end + end + else + @items = nil + @total_results = 0 + end + + erb :'search' +end diff --git a/public/img/hotcat.svg b/public/img/hotcat.svg new file mode 100644 index 00000000..f3ebb274 --- /dev/null +++ b/public/img/hotcat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sass/_project-sass/_project-Main.scss b/sass/_project-sass/_project-Main.scss index 8e9f5de9..ec1ab3b7 100644 --- a/sass/_project-sass/_project-Main.scss +++ b/sass/_project-sass/_project-Main.scss @@ -2674,6 +2674,60 @@ a.tag:hover { .browse-search-page .filter { width: 100%; } +.search-home-page { + align-items: center; + background: + radial-gradient(circle at 50% 20%, rgba(123, 202, 197, .32), rgba(123, 202, 197, 0) 260px), + linear-gradient(180deg, rgba(255,255,255,.8), rgba(255,255,255,0) 64%); + display: flex; + min-height: 535px; + padding: 54px 0 70px 0; +} +.search-home-content { + box-sizing: border-box; + padding-bottom: 0; + padding-top: 0; + width: 100%; +} +.search-home { + margin: 0 auto; + max-width: 720px; + text-align: center; +} +.search-mascot { + display: block; + filter: drop-shadow(0 10px 18px rgba(58, 71, 71, .13)); + height: auto; + margin: 0 auto 12px auto; + max-width: 360px; + width: 66%; +} +.search-results-mascot-frame { + display: block; + height: 62px; + margin: 0 0 6px 0; + overflow: hidden; + position: relative; + width: 230px; +} +.search-results-mascot { + display: block; + filter: drop-shadow(0 4px 8px rgba(0,0,0,.18)); + height: auto; + left: 50%; + max-width: none; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 230px; + z-index: 1; +} +.search-tagline { + color: #425c5a; + font-size: 1.28em; + margin: 0 0 28px 0; + text-shadow: 0 1px 0 rgba(255,255,255,.75); +} .browse-search-form { margin: 0; @@ -2706,6 +2760,88 @@ a.tag:hover { padding: 0 18px; } } +.search-home-form { + margin-left: auto; + margin-right: auto; + max-width: 650px; + + fieldset { + display: grid; + gap: 18px; + justify-items: center; + } +} +.search-home-field { + position: relative; + width: 100%; + + .fa { + color: #6a9a96; + font-size: 1.25em; + left: 19px; + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 1; + } + + .input-Area { + background: rgba(255,255,255,.94); + border: 2px solid rgba(104, 184, 178, .6); + border-radius: 30px; + box-shadow: 0 8px 24px rgba(33, 64, 61, .12), 0 1px 0 rgba(255,255,255,.95) inset; + box-sizing: border-box; + color: #333; + font-size: 1.2em; + height: 56px!important; + line-height: 24px; + margin: 0; + padding: 12px 18px 12px 52px; + width: 100%; + + &:focus { + border-color: #42b7ae; + box-shadow: 0 10px 28px rgba(33, 64, 61, .16), 0 0 0 3px rgba(66, 183, 174, .18); + } + } +} +.search-home-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + + .btn-Action { + height: 42px; + line-height: 42px; + margin: 0; + min-width: 152px; + padding: 0 18px; + } + + .btn-Secondary { + background: #f7fbfa; + border: 1px solid rgba(88, 163, 157, .55); + box-shadow: 0 2px 5px rgba(0,0,0,.08); + color: #3e6864!important; + text-shadow: none; + + &:hover, + &:focus { + background: #e8f4f2; + border-color: rgba(66, 183, 174, .85); + color: #284f4b!important; + } + + &:visited { + color: #3e6864!important; + } + } +} +.search-message { + font-size: 1.1em; + margin: 0; +} .browse-search-page .search-results { background: #fff; } @@ -2780,22 +2916,66 @@ a.tag:hover { } .browse-search-page .row.content { + box-sizing: border-box; padding-bottom: 16px; + padding-left: 5.4%; + padding-right: 5.4%; padding-top: 14px; } + .interior .header-Outro.with-columns.browse-search-page .col { + box-sizing: border-box; + padding-left: 0; + padding-right: 0; + } + .browse-search-page h1 { font-size: 2.6em; line-height: 1.05; margin-bottom: 16px; } + .search-home-page { + min-height: 390px; + padding-bottom: 44px; + padding-top: 44px; + } + + .search-mascot { + max-width: 280px; + width: 82%; + } + + .search-results-mascot-frame { + height: 56px; + margin: 0 auto 6px auto; + width: 205px; + } + + .search-results-mascot { + width: 205px; + } + + .search-tagline { + font-size: 1.05em; + margin-bottom: 18px; + } + + .search-home-field .input-Area { + height: 50px!important; + padding-left: 46px; + } + .browse-search-form { + width: 100%; + fieldset { align-items: stretch; + box-sizing: border-box; display: grid; gap: 10px; justify-items: center; + width: 100%; } .input-Area { @@ -2821,6 +3001,11 @@ a.tag:hover { } } + .search-home-form .search-home-field .input-Area { + height: 50px!important; + padding-left: 46px; + } + .browse-search-page .result-item { gap: 10px; grid-template-columns: 1fr; diff --git a/tests/acceptance/search_tests.rb b/tests/acceptance/search_tests.rb new file mode 100644 index 00000000..e7a33b95 --- /dev/null +++ b/tests/acceptance/search_tests.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require_relative './environment.rb' + +describe '/search' do + include Capybara::DSL + include Capybara::Minitest::Assertions + include Rack::Test::Methods + + def app + Sinatra::Application + end + + before do + Capybara.default_driver = :rack_test + Capybara.reset_sessions! + $redis_cache.del('search_query_count') + end + + it 'shows a playful standalone search page' do + visit '/search' + + _(page).must_have_selector 'img.search-mascot[src="/img/hotcat.svg"][alt="Hot Cat"]' + _(page).must_have_content 'Search the handmade web' + _(page).must_have_selector 'form[action="/search"] input[name="q"]' + _(page).must_have_link 'Random Site', href: '/browse?sort_by=random' + end + + it 'links to search immediately after websites in the top nav' do + visit '/search' + + nav_links = all('.constant-Nav a').map(&:text) + _(nav_links[nav_links.index('Websites') + 1]).must_equal 'Search' + end + + it 'does not mount the old browse search path' do + visit '/browse/search' + + _(page.status_code).must_equal 404 + end + + it 'redirects post search submissions to the get search page' do + post '/search', q: 'weird web' + + _(last_response.status).must_equal 302 + _(last_response.headers['Location']).must_equal 'http://example.org/search?q=weird+web' + end + + it 'shows the mascot on the results header' do + $redis_cache.set('search_query_count', $config['google_custom_search_query_limit'], ex: 86400) + + visit '/search?q=web' + + _(page).must_have_selector '.header-Outro img.search-results-mascot[src="/img/hotcat.svg"][alt="Hot Cat"]' + _(page).wont_have_selector '.header-Outro h1', text: 'Site Search' + end +end diff --git a/views/_header_links.erb b/views/_header_links.erb index 6f81db38..10bd09ca 100644 --- a/views/_header_links.erb +++ b/views/_header_links.erb @@ -34,6 +34,9 @@ }); +
  • + Search +
  • <% unless is_education? %>
  • Activity diff --git a/views/browse.erb b/views/browse.erb index 4abb10b4..93c891cf 100644 --- a/views/browse.erb +++ b/views/browse.erb @@ -154,41 +154,11 @@ <% end %> <% if params[:sort_by] != 'moderation' %> <% unless is_education? %> -
    -

    Site Search

    - - <% if !daily_search_max? %> -
    -
    - - -
    -
    - <% else %> -
    -
    - - -
    -
    -

    Search powered by Duck Duck Go

    - <% end %> -
    <%== erb :_browse_tags, layout: false %> <% end %> <% end %> - - <% if signed_in? && current_site.is_admin %>