diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 29cff32..3b79da4 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -1,4 +1,4 @@ -name: "matsue_onsen_map" +name: "matsue_gourmet_map" services: rails-app: @@ -7,9 +7,8 @@ services: dockerfile: .devcontainer/Dockerfile volumes: - - ../..:/workspaces:cached - - # Overrides default command so things don't shut down after the process ends. + - ../..:/workspaces:cached + # Overrides default command so things don't shut down after the process ends. command: sleep infinity # Uncomment the next line to use a non-root user for all processes. @@ -18,8 +17,8 @@ services: # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. # (Adding the "ports" property to this file will not forward from a Codespace.) depends_on: - - selenium - - postgres + - selenium + - postgres selenium: image: selenium/standalone-chromium @@ -29,12 +28,12 @@ services: image: postgres:16.1 restart: unless-stopped networks: - - default + - default volumes: - - postgres-data:/var/lib/postgresql/data + - postgres-data:/var/lib/postgresql/data environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres volumes: postgres-data: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8a7b647..b280054 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,11 +1,10 @@ // For format details, see https://containers.dev/implementors/json_reference/. // For config options, see the README at: https://github.com/devcontainers/templates/tree/main/src/ruby { - "name": "matsue_onsen_map", + "name": "matsue_gourmet_map", "dockerComposeFile": "compose.yaml", "service": "rails-app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - // Features to add to the dev container. More info: https://containers.dev/features. "features": { "ghcr.io/devcontainers/features/github-cli:1": {}, @@ -13,24 +12,21 @@ "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/rails/devcontainer/features/postgres-client": {} }, - "containerEnv": { "CAPYBARA_SERVER_PORT": "45678", "SELENIUM_HOST": "selenium", "KAMAL_REGISTRY_PASSWORD": "$KAMAL_REGISTRY_PASSWORD", "DB_HOST": "postgres" }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [3000, 5432], - + "forwardPorts": [ + 3000, + 5432 + ], // Configure tool-specific properties. // "customizations": {}, - // Uncomment to connect as root instead. More info: https://containers.dev/implementors/json_reference/#remoteUser. // "remoteUser": "root", - - // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "bin/setup --skip-server" -} +} \ No newline at end of file diff --git a/Gemfile b/Gemfile index b4fab18..fbafbc0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,11 @@ source "https://rubygems.org" +# 地図gem +gem "geocoder" + +# dotenv +gem "dotenv-rails", groups: [ :development, :test ] + # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 8.0.2" # The modern asset pipeline for Rails [https://github.com/rails/propshaft] diff --git a/Gemfile.lock b/Gemfile.lock index a558116..cac4c9a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,11 +103,15 @@ GEM concurrent-ruby (1.3.5) connection_pool (2.5.3) crass (1.0.6) + csv (3.3.5) date (3.4.1) debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) dotenv (3.1.8) + dotenv-rails (3.1.8) + dotenv (= 3.1.8) + railties (>= 6.1) drb (2.2.3) ed25519 (1.4.0) erb (5.0.2) @@ -123,6 +127,9 @@ GEM fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) + geocoder (1.8.5) + base64 (>= 0.1.0) + csv (>= 3.0.0) globalid (1.2.1) activesupport (>= 6.1) htmlbeautifier (1.4.3) @@ -386,6 +393,8 @@ DEPENDENCIES brakeman capybara debug + dotenv-rails + geocoder htmlbeautifier image_processing (~> 1.2) importmap-rails diff --git a/README.md b/README.md index cbba4e4..837ae25 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# 松江温泉マップ 🗺️♨️ +# 松江グルメマップ 🗺️♨️ ## プロジェクト概要 -このアプリケーションは、島根県松江市の温泉を登録・閲覧できるWebアプリケーションです。 +このアプリケーションは、島根県松江市のグルメを登録・閲覧できるWebアプリケーションです。 ### 主な機能 -- 🗺️ **温泉マップ表示**: 登録された温泉を地図上で確認できます -- ♨️ **温泉登録**: 新しい温泉スポットを登録できます -- ⭐ **レビュー機能**: 温泉にレビューを投稿できます +- 🗺️ **グルメマップ表示**: 登録されたグルメを地図上で確認できます +- ♨️ **グルメ登録**: 新しいグルメスポットを登録できます +- ⭐ **レビュー機能**: グルメにレビューを投稿できます - 📱 **レスポンシブデザイン**: 複数の画面幅に対応しています ## 技術スタック @@ -97,7 +97,7 @@ matsue-onsen-map/ ## 開発の流れ -1. **機能の追加**: 新しい温泉登録機能を追加する場合 +1. **機能の追加**: 新しいグルメ登録機能を追加する場合 2. **モデルの作成**: `rails generate model` でデータベーステーブルを作成 3. **コントローラーの作成**: `rails generate controller` で画面処理を作成 4. **ビューの作成**: HTMLテンプレートを作成 diff --git a/app/controllers/admin/admin_controller.rb b/app/controllers/admin/admin_controller.rb new file mode 100644 index 0000000..e8debbf --- /dev/null +++ b/app/controllers/admin/admin_controller.rb @@ -0,0 +1,15 @@ +# app/controllers/admin/admin_controller.rb +class Admin::AdminController < ApplicationController + before_action :require_admin_password + + + + + + private + + def require_admin_password + return if session[:admin_authenticated] + redirect_to admin_login_path + end +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb new file mode 100644 index 0000000..61c9a14 --- /dev/null +++ b/app/controllers/admin/base_controller.rb @@ -0,0 +1,2 @@ +class Admin::BaseController < ApplicationController +end diff --git a/app/controllers/admin/onsens_controller.rb b/app/controllers/admin/onsens_controller.rb index 021ccb6..bbc022e 100644 --- a/app/controllers/admin/onsens_controller.rb +++ b/app/controllers/admin/onsens_controller.rb @@ -1,12 +1,14 @@ -class Admin::OnsensController < ApplicationController +class Admin::OnsensController < Admin::AdminController before_action :set_onsen, only: %i[ show edit update destroy ] - # GET /onsens or /onsens.json + # GET /onsens def index + session[:admin] = nil + @onsens = Onsen.all end - # GET /onsens/1 or /onsens/1.json + # GET /onsens/1 def show end @@ -19,7 +21,7 @@ def new def edit end - # POST /onsens or /onsens.json + # POST /onsens def create @onsen = Onsen.new(onsen_params) @@ -34,10 +36,17 @@ def create end end - # PATCH/PUT /onsens/1 or /onsens/1.json + # PATCH/PUT /onsens/1 def update respond_to do |format| - if @onsen.update(onsen_params) + if @onsen.update(onsen_params.except(:remove_image_ids)) + if onsen_params[:remove_image_ids].present? + onsen_params[:remove_image_ids].each do |image_id| + image = @onsen.images.find_by(id: image_id) + image.purge if image + end + end + format.html { redirect_to admin_onsen_path(@onsen), notice: "Onsen was successfully updated." } format.json { render :show, status: :ok, location: @onsen } else @@ -47,7 +56,8 @@ def update end end - # DELETE /onsens/1 or /onsens/1.json + + # DELETE /onsens/1 def destroy @onsen.destroy! @@ -58,13 +68,14 @@ def destroy end private - # Use callbacks to share common setup or constraints between actions. - def set_onsen - @onsen = Onsen.find(params.expect(:id)) - end - # Only allow a list of trusted parameters through. - def onsen_params - params.expect(onsen: [ :name, :geo_lat, :geo_lng, :description, :tags, images: [] ]) - end + # Use callbacks to share common setup or constraints between actions. + def set_onsen + @onsen = Onsen.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def onsen_params + params.require(:onsen).permit(:name, :geo_lat, :geo_lng, :description, :tags, :url, :sales_s, :sales_f, :holiday, :address, :price, images: [], remove_image_ids: []) + end end diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb new file mode 100644 index 0000000..6c825e8 --- /dev/null +++ b/app/controllers/admin/sessions_controller.rb @@ -0,0 +1,27 @@ +# app/controllers/admin/sessions_controller.rb +class Admin::SessionsController < ApplicationController + def new + # パスワード入力フォーム + end + + def create + if params[:password] == ENV["ADMIN_PASSWORD"] + session[:admin_authenticated] = true + redirect_to admin_root_path, notice: "ログイン成功" + else + flash[:alert] = "パスワードが違います" + render :new + end + end + + def destroy + session.delete(:admin_authenticated) + redirect_to root_path, notice: "ログアウトしました" + end + + + # セッションリセット + def reset_admin_session + session[:admin_authenticated] = nil + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cbbb1a7..70faae5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,10 @@ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # Disable browser restrictions in development environment allow_browser versions: :modern unless Rails.env.development? + + private + + def reset_admin_session + session.delete(:admin_authenticated) + end end diff --git a/app/controllers/onsens_controller.rb b/app/controllers/onsens_controller.rb index c5c2b63..15b09ac 100644 --- a/app/controllers/onsens_controller.rb +++ b/app/controllers/onsens_controller.rb @@ -2,13 +2,26 @@ class OnsensController < ApplicationController before_action :set_onsen, only: %i[ show ] # GET /onsens or /onsens.json def index - # Strong Parametersで安全なパラメータのみ許可 - @search_params = params.permit(:q, :tags) + # Strong Parametersで安全なパラメータのみ許可(boundsを追加) + @search_params = params.permit(:q, :tags, :bounds) # モデルの検索メソッドを呼び出し、新しい順でソート @onsens = Onsen.search(@search_params).order(created_at: :desc) + + # 地図の表示範囲で絞り込み + if @search_params[:bounds].present? + bounds = @search_params[:bounds].split(",") + @onsens = @onsens.within_bounds(bounds) + end + + # セッションリセット + reset_admin_session end + + + + # GET /onsens/1 or /onsens/1.json def show @reviews = @onsen.reviews.order(created_at: :desc) @@ -18,4 +31,8 @@ def show def set_onsen @onsen = Onsen.find(params[:id]) end + + def onsen_params + params.require(:onsen).permit(:name, :sales_s, :sales_f, :holiday, :other_columns) + end end diff --git a/app/helpers/admin/admin_helper.rb b/app/helpers/admin/admin_helper.rb new file mode 100644 index 0000000..53c4d49 --- /dev/null +++ b/app/helpers/admin/admin_helper.rb @@ -0,0 +1,2 @@ +module Admin::AdminHelper +end diff --git a/app/helpers/admin/base_helper.rb b/app/helpers/admin/base_helper.rb new file mode 100644 index 0000000..f24ad87 --- /dev/null +++ b/app/helpers/admin/base_helper.rb @@ -0,0 +1,2 @@ +module Admin::BaseHelper +end diff --git a/app/helpers/admin/sessions_helper.rb b/app/helpers/admin/sessions_helper.rb new file mode 100644 index 0000000..760cb3d --- /dev/null +++ b/app/helpers/admin/sessions_helper.rb @@ -0,0 +1,2 @@ +module Admin::SessionsHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 027ac56..b0ca5fa 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -20,7 +20,7 @@ def public_navigation_links(current_path) # @return [Array] 管理サイト用ナビゲーションリンク def admin_navigation_links(current_path) create_navigation_link_set([ - { path: "/admin/onsens", label: t("views.navigation.onsen_management"), aria_label: "温泉管理", current_check: -> { current_path.start_with?("/admin/onsens") } }, + { path: "/admin/onsens", label: t("views.navigation.onsen_management"), aria_label: "管理画面", current_check: -> { current_path.start_with?("/admin/onsens") } }, { path: "/", label: t("views.navigation.public_site"), aria_label: "公開サイト", current_check: -> { false } } ]) end diff --git a/app/javascript/controllers/map_controller.js b/app/javascript/controllers/map_controller.js index 5e9e8b1..fb537d6 100644 --- a/app/javascript/controllers/map_controller.js +++ b/app/javascript/controllers/map_controller.js @@ -4,52 +4,141 @@ import L from "leaflet" // Connects to data-controller="map" export default class extends Controller { static targets = ["container"] - static values = { onsens: Array } + static values = { + onsens: Array + } connect() { - this.onsens = this._parseOnsensData(); - console.log(this.onsens); + console.log("Map controller connected!"); // 追加 + console.log("Container target:", this.containerTarget); // 追加 + + this.userLocationMarker = null; // 現在地マーカーを保持するプロパティ + + if (!this.containerTarget) { // 追加 + console.error("Map container target is missing!"); // 追加 + return; // 追加: ターゲットがない場合はこれ以上処理しない + } this.map = L.map(this.containerTarget).setView([35.468, 133.0483], 11.5); + console.log("Leaflet map initialized:", this.map); // 追加 L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(this.map); + console.log("Tile layer added."); // 追加 - const onsenIcon = L.icon({ - iconUrl: '/onsen.svg', - iconSize: [32, 32], - iconAnchor: [16, 32], - }); + // 温泉マーカーを保持するレイヤーグループを作成 + this.onsenMarkers = L.layerGroup().addTo(this.map); + console.log("Onsen markers layer group created."); // 追加 + this._plotOnsens(); // 初期マーカーをプロット + console.log("Initial onsens plotted."); // 追加 + } + + /** + * ユーザーの現在地を取得して地図に表示します。 + * HTML側で のようにして呼び出します。 + */ + locateUser() { + if (!navigator.geolocation) { + alert("お使いのブラウザは現在地取得機能に対応していません。"); + return; + } - this.onsens.forEach(onsen => { - L.marker([onsen.geo_lat, onsen.geo_lng], { icon: onsenIcon }) - .addTo(this.map) - .bindPopup(onsen.name); + navigator.geolocation.getCurrentPosition( + (position) => this._handleGeolocationSuccess(position), + (error) => this._handleGeolocationError(error), + { + enableHighAccuracy: true, // 高精度な位置情報を要求 + timeout: 10000, // 10秒でタイムアウト + maximumAge: 0 // キャッシュを使用しない + } + ); + } + + /** + * 温泉データを地図にプロットする + * @private + */ + _plotOnsens() { + console.log("Plotting onsens. Data:", this.onsensValue); + this.onsenMarkers.clearLayers(); + + this.onsensValue.forEach(onsen => { + if (onsen.geo_lat && onsen.geo_lng) { + const onsenIcon = L.icon({ + // status_icon があるならそれを使い、なければデフォルト画像 + iconUrl: onsen.status_icon ? `/${onsen.status_icon}.svg` : '/onsen.png', + iconSize: [32, 32], + iconAnchor: [16, 32], + }); + + L.marker([onsen.geo_lat, onsen.geo_lng], { icon: onsenIcon }) + .bindPopup(onsen.name) + .addTo(this.onsenMarkers); + } else { + console.warn("Onsen missing geo_lat or geo_lng:", onsen); + } }); + + console.log("Onsens plotting complete."); } + disconnect() { + console.log("Map controller disconnected."); // 追加 if (this.map) { this.map.remove(); this.map = null; + console.log("Map removed."); // 追加 } } - // === プライベートメソッド(内部処理用) === /** - * HTML要素から温泉データを取得・パース - * @returns {Array} 温泉データの配列 + * 現在地取得成功時の処理 + * @param {GeolocationPosition} position + * @private + */ + _handleGeolocationSuccess(position) { + const { latitude, longitude } = position.coords; + const userLatLng = L.latLng(latitude, longitude); + + // 地図の中心を現在地に移動し、ズームレベルを調整 + this.map.setView(userLatLng, 15); + + // 既存の現在地マーカーがあれば削除 + if (this.userLocationMarker) { + this.userLocationMarker.remove(); + } + + // 新しいマーカーを作成して追加 (Leafletのデフォルトアイコンを使用) + this.userLocationMarker = L.marker(userLatLng) + .addTo(this.map) + .bindPopup("現在地") + .openPopup(); + } + + + /** + * 現在地取得失敗時の処理 + * @param {GeolocationPositionError} error + * @private */ - _parseOnsensData() { - try { - const rawData = this.element.dataset.mapOnsens || "[]"; - return JSON.parse(rawData); - } catch (error) { - console.warn("温泉データのパースに失敗:", error); - return []; + _handleGeolocationError(error) { + let message = "現在地の取得に失敗しました。"; + switch (error.code) { + case error.PERMISSION_DENIED: + message = "位置情報の利用が許可されていません。ブラウザの設定を確認してください。"; + break; + case error.POSITION_UNAVAILABLE: + message = "位置情報を取得できませんでした。"; + break; + case error.TIMEOUT: + message = "位置情報の取得がタイムアウトしました。"; + break; } + alert(message); + console.error("Geolocation error:", error); } } diff --git a/app/models/onsen.rb b/app/models/onsen.rb index 46d33c5..77192ee 100644 --- a/app/models/onsen.rb +++ b/app/models/onsen.rb @@ -10,6 +10,18 @@ class Onsen < ApplicationRecord size: { less_than: 5.megabytes }, # 1枚あたり5MB未満 limit: { max: 5 } # 最大3枚まで + # 住所変換するやつ。 + geocoded_by :address, latitude: :geo_lat, longitude: :geo_lng + after_validation :geocode, if: :will_save_change_to_address? + + # @example Onsen.within_bounds([sw_lat, sw_lng, ne_lat, ne_lng]) + scope :within_bounds, ->(bounds) { + return all if bounds.blank? + sw_lat, sw_lng, ne_lat, ne_lng = bounds.map(&:to_f) + where(geo_lat: sw_lat..ne_lat, geo_lng: sw_lng..ne_lng) + } + + # @example Onsen.search(q: "玉造", tags: "露天風呂", lat: 35.1, lng: 132.5, radius_km: 10) def self.search(params) @@ -50,6 +62,39 @@ def self.apply_tag_search(scope, tags_param) scope.where(tag_query, *tag_values) end + # 価格が0円以上のみ登録可能 + class Onsen < ApplicationRecord + has_many_attached :images + has_many :reviews + + # ----------------------------- + # ここから追加 + # 料金の整数化(空欄は nil に) + before_validation :normalize_price + + private + + def normalize_price + if price.present? && price.to_s.strip != "" + self.price = price.to_i + else + self.price = nil + end + end + # ここまで追加 + # ----------------------------- + + # バリデーション + validates :name, presence: true + validates :price, + numericality: { + only_integer: true, + greater_than: 0, + message: "は1円以上の整数で入力してください" + }, + allow_nil: true + end + # 位置情報検索:二段階フィルタによる距離絞り込み # # @param scope [ActiveRecord::Relation] 検索対象スコープ @@ -105,10 +150,84 @@ def self.apply_rectangular_filter(scope, lat, lng, radius) # @param lat [Float] 基準緯度 # @param lng [Float] 基準経度 # @param radius [Float] 検索半径(km) - # @return [Array] 距離条件を満たす温泉配列 + # @return [Array] 距離条件を満たすグルメ配列 def self.apply_precise_distance_filter(scope, lat, lng, radius) scope.select do |onsen| DistanceCalculatorService.calculate(lat, lng, onsen.geo_lat, onsen.geo_lng) <= radius end end + + def open_now + if sales_s.blank? || sales_f.blank? + return "営業時間不明" + end + + Time.zone = "Tokyo" + now = Time.zone.now + wday = %w[日 月 火 水 木 金 土][now.wday] # 現在の曜日を取得 + # holidayが空でなく、かつその曜日が含まれていたら休業 + + return "休業中(定休日)" if holiday.present? && holiday.include?(wday) + + now_sec = now.hour * 3600 + now.min * 60 + open_sec = sales_s.to_time.hour * 3600 + sales_s.to_time.min * 60 + close_sec = sales_f.to_time.hour * 3600 + sales_f.to_time.min * 60 + + if open_sec > close_sec + close_sec += 24 * 3600 + end + + if close_sec - now_sec <= 30.minutes && now_sec < close_sec + "まもなく閉店 (閉店まであと#{(close_sec - now_sec) / 60}分)" + + elsif open_sec <= now_sec && now_sec <= close_sec + "営業中" + + elsif now_sec < open_sec && open_sec - now_sec <= 30.minutes + "まもなく開店 (開店まであと#{(open_sec - now_sec) / 60}分)" + + else + sales_s_time = sales_s.to_time + if now_sec < open_sec + # 今日の開店前 + "営業時間外 (#{sales_s_time.strftime('%H:%M')} から開店)" + else + # 今日の営業は終了 → 翌日の開店まで待つ + "営業時間外 (明日 #{sales_s_time.strftime('%H:%M')} に開店)" + end + end + end + + # 営業状態アイコン + def status_icon + return "close" if sales_s.blank? || sales_f.blank? + + Time.zone = "Tokyo" + now = Time.zone.now + + now_sec = now.hour * 3600 + now.min * 60 + open_sec = sales_s.to_time.hour * 3600 + sales_s.to_time.min * 60 + close_sec = sales_f.to_time.hour * 3600 + sales_f.to_time.min * 60 + + if open_sec > close_sec + close_sec += 24 * 3600 + end + + if open_sec <= now_sec && now_sec <= close_sec + if close_sec - now_sec <= 30.minutes + "close_soon" + else + "open" + end + elsif now_sec < open_sec + if open_sec - now_sec <= 30.minutes + "open_soon" + else + "close" + end + else + # 今日の営業終了 → 翌日の開店待ち + "close" + end + end end diff --git a/app/views/admin/onsens/_form.html.erb b/app/views/admin/onsens/_form.html.erb index 115ea4f..4d3b224 100644 --- a/app/views/admin/onsens/_form.html.erb +++ b/app/views/admin/onsens/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with(model: [:admin, onsen || @onsen], html: { multipart: true, class: "space-y-6" }) do |form| %> +<%= form_with model: [:admin, @onsen], local: true do |form| %> <% if onsen.errors.any? %>

<%= pluralize(onsen.errors.count, "件のエラー") %>があります:

@@ -13,15 +13,19 @@ <%= form.label :name, '名称', class: 'block font-semibold mb-1' %> <%= form.text_field :name, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %>
-
-
- <%= form.label :geo_lat, '緯度', class: 'block font-semibold mb-1' %> - <%= form.text_field :geo_lat, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %> -
-
- <%= form.label :geo_lng, '経度', class: 'block font-semibold mb-1' %> - <%= form.text_field :geo_lng, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %> -
+ <%= form.label :address, '住所', class: 'block font-semibold mb-1' %> + <%= form.text_field :address, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %> +
+ <%= form.label :sales_s, '始業時間', class: 'block font-semibold mb-1' %> + <%= form.text_field :sales_s, class: "block shadow-sm rouded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %> +
+
+ <%= form.label :sales_f, '終業時間', class: 'block font-semibold mb-1' %> + <%= form.text_field :sales_f, class: "block shadow-sm rouded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %> +
+
+ <%= form.label :holiday, '定休日(カンマ区切り)', class: 'block font-semibold mb-1' %> + <%= form.text_field :holiday, class: "block shadow-sm rouded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %>
<%= form.label :description, '説明', class: 'block font-semibold mb-1' %> @@ -31,6 +35,14 @@ <%= form.label :tags, 'タグ(カンマ区切り)', class: 'block font-semibold mb-1' %> <%= form.text_field :tags, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600" %>
+
+ <%= form.label :price, '価格帯(円)', class: 'block font-semibold mb-1' %> + <%= form.number_field :price, min: 1, step: 1, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600", placeholder: "例 1500" %> +
+
+ <%= form.label :url, 'リンク(公式サイト)', class: 'block font-semibold mb-1' %> + <%= form.text_field :url, class: "block shadow-sm rounded-md border px-3 py-2 w-full border-gray-300 focus:outline-blue-600", placeholder: "任意" %> +
<%= form.label :images, '画像(最大5枚・JPEG/PNG/GIF)', class: 'block font-semibold mb-1' %> <%= form.file_field :images, multiple: true, accept: 'image/jpeg,image/png,image/gif', class: "block border rounded px-2 py-1 w-full" %> diff --git a/app/views/admin/onsens/_onsen.html.erb b/app/views/admin/onsens/_onsen.html.erb index 98dd7d3..a3bf883 100644 --- a/app/views/admin/onsens/_onsen.html.erb +++ b/app/views/admin/onsens/_onsen.html.erb @@ -1,12 +1,36 @@
名称
<%= onsen.name %>
+ +
住所
+
<%= onsen.address %>
+
緯度
<%= onsen.geo_lat %>
+
経度
<%= onsen.geo_lng %>
+ +
URL
+
<%= onsen.url %>
+ +
始業時間
+
<%= onsen.sales_s %>
+ +
終業時間
+
<%= onsen.sales_f %>
+ +
営業状態
+
<%= onsen.open_now %>
+ +
定休日
+
<%= onsen.holiday %>
+
説明
<%= onsen.description %>
+
タグ
<%= onsen.tags %>
+
料金
+
<%= onsen.price %>
diff --git a/app/views/admin/onsens/edit.html.erb b/app/views/admin/onsens/edit.html.erb index 56bd4d9..157fbc6 100644 --- a/app/views/admin/onsens/edit.html.erb +++ b/app/views/admin/onsens/edit.html.erb @@ -1,9 +1,9 @@ -<% content_for :title, '温泉情報編集' %> +<% content_for :title, 'グルメ情報編集' %>
-

温泉情報編集

+

グルメ情報編集

<%= render "form", onsen: @onsen %>
- <%= link_to 'この温泉を表示', onsen_path(@onsen), target: "_blank", rel: "noopener", class: "rounded-md px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium w-full sm:w-auto text-center" %> + <%= link_to 'このグルメを表示', onsen_path(@onsen), target: "_blank", rel: "noopener", class: "rounded-md px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium w-full sm:w-auto text-center" %> <%= link_to '一覧に戻る', admin_onsens_path, class: "rounded-md px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium w-full sm:w-auto text-center" %>
diff --git a/app/views/admin/onsens/index.html.erb b/app/views/admin/onsens/index.html.erb index 3948bbf..887b2a7 100644 --- a/app/views/admin/onsens/index.html.erb +++ b/app/views/admin/onsens/index.html.erb @@ -1,11 +1,11 @@ -<% content_for :title, t('admin.onsens.index.title', default: '温泉一覧') %> +<% content_for :title, t('admin.onsens.index.title', default: 'グルメ一覧') %>
<% if notice.present? %>

<%= notice %>

<% end %>
-

<%= t('admin.onsens.index.title', default: '温泉一覧') %>

- <%= link_to t('admin.onsens.index.new', default: '新規温泉登録'), new_admin_onsen_path, class: "rounded-md px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow w-full md:w-auto text-center" %> +

<%= t('admin.onsens.index.title', default: 'グルメ一覧') %>

+ <%= link_to t('admin.onsens.index.new', default: '新規グルメ登録'), new_admin_onsen_path, class: "rounded-md px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow w-full md:w-auto text-center" %>
<% if @onsens.any? %> @@ -30,7 +30,7 @@
<% end %> <% else %> -

温泉データがありません。

+

グルメデータがありません。

<% end %>
diff --git a/app/views/admin/onsens/new.html.erb b/app/views/admin/onsens/new.html.erb index b81a74a..8093b34 100644 --- a/app/views/admin/onsens/new.html.erb +++ b/app/views/admin/onsens/new.html.erb @@ -1,6 +1,6 @@ -<% content_for :title, '温泉新規登録' %> +<% content_for :title, 'グルメ新規登録' %>
-

温泉新規登録

+

グルメ新規登録

<%= render "form", onsen: @onsen %>
<%= link_to '一覧に戻る', admin_onsens_path, class: "rounded-md px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium w-full sm:w-auto text-center" %> diff --git a/app/views/admin/onsens/show.html.erb b/app/views/admin/onsens/show.html.erb index b6173ef..87200b1 100644 --- a/app/views/admin/onsens/show.html.erb +++ b/app/views/admin/onsens/show.html.erb @@ -1,9 +1,9 @@ -<% content_for :title, '温泉詳細' %> +<% content_for :title, 'グルメ詳細' %>
<% if notice.present? %>

<%= notice %>

<% end %> -

温泉詳細

+

グルメ詳細

<%= render partial: 'onsen', locals: { onsen: @onsen } %> <% if @onsen.images.attached? %>
diff --git a/app/views/admin/sessions/new.html.erb b/app/views/admin/sessions/new.html.erb new file mode 100644 index 0000000..8ecd8a8 --- /dev/null +++ b/app/views/admin/sessions/new.html.erb @@ -0,0 +1,20 @@ +
+
+

管理画面ログイン

+ <% if flash[:alert] %> +

<%= flash[:alert] %>

+ <% end %> + <%= form_with url: admin_login_path, method: :post, local: true do |form| %> +
+ <%= form.label :password, "パスワード", class: "block text-gray-700 font-medium mb-2" %> + <%= form.password_field :password, class: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400" %> +
+
+ <%= form.submit "ログイン", class: "w-full bg-blue-600 hover:bg-blue-500 text-white font-semibold py-2 rounded-lg transition-colors duration-200" %> +
+ <% end %> +

+ 管理画面にアクセスするにはパスワードが必要です +

+
+
diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index fec9096..22d2241 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -4,6 +4,8 @@ 利用規約 お問い合わせ
-
© <%= Time.current.year %> 松江温泉マップ. All rights reserved.
+ +
© <%= Time.current.year %> 松江グルメマップ. All rights reserved.
+
diff --git a/app/views/onsens/_location_search_form.html.erb b/app/views/onsens/_location_search_form.html.erb index fc82bb7..815d097 100644 --- a/app/views/onsens/_location_search_form.html.erb +++ b/app/views/onsens/_location_search_form.html.erb @@ -13,7 +13,7 @@ id="zipcode" name="zipcode" value="<%= params[:zipcode] %>" - class="flex-1 border rounded px-3 py-2" + class="min-w-0 flex-1 border rounded px-3 py-2" placeholder="例: 6900874" maxlength="8" aria-label="<%= t('views.onsens.index.zipcode') %>" @@ -38,7 +38,7 @@ id="address" name="address" value="<%= params[:address] %>" - class="flex-1 border rounded px-3 py-2" + class="min-w-0 flex-1 border rounded px-3 py-2" placeholder="例: 松江市朝日町" aria-label="<%= t('views.onsens.index.address', default: '住所') %>" data-zipcode-target="address" diff --git a/app/views/onsens/_onsen.html.erb b/app/views/onsens/_onsen.html.erb index c1ec7cc..2b56c64 100644 --- a/app/views/onsens/_onsen.html.erb +++ b/app/views/onsens/_onsen.html.erb @@ -19,4 +19,8 @@ Tags: <%= onsen.tags %>
+
+ price: + <%= onsen.price %> +
diff --git a/app/views/onsens/_search_form.html.erb b/app/views/onsens/_search_form.html.erb index 3835003..4b7aabe 100644 --- a/app/views/onsens/_search_form.html.erb +++ b/app/views/onsens/_search_form.html.erb @@ -1,20 +1,23 @@ -<%# 温泉検索フォーム用パーシャル %> +<%# グルメ検索フォーム用パーシャル %> <%# 複合検索機能を提供:テキスト検索 + タグ検索 + 位置情報検索 %> -