From e85da718d5d728ef8e7d3e19332c94ea83759a3c Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:04:15 +0900 Subject: [PATCH 01/32] =?UTF-8?q?useSearchParams.js=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/hooks/useSearchParams.js | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 app/javascript/hooks/useSearchParams.js diff --git a/app/javascript/hooks/useSearchParams.js b/app/javascript/hooks/useSearchParams.js new file mode 100644 index 00000000000..2d64fb5bf95 --- /dev/null +++ b/app/javascript/hooks/useSearchParams.js @@ -0,0 +1,75 @@ +import React from 'react' + +function createSearchParams(init = '') { + return new URLSearchParams( + typeof init === 'string' || + Array.isArray(init) || + init instanceof URLSearchParams + ? init + : Object.keys(init).reduce((memo, key) => { + const value = init[key] + return memo.concat( + Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]] + ) + }, []) + ) +} + +function getSearchParamsForLocation(locationSearch, defaultSearchParams) { + const searchParams = createSearchParams(locationSearch) + + if (defaultSearchParams) { + defaultSearchParams.forEach((_, key) => { + if (!searchParams.has(key)) { + defaultSearchParams.getAll(key).forEach((value) => { + searchParams.append(key, value) + }) + } + }) + } + + return searchParams +} + +/** + * @see https://reactrouter.com/en/main/hooks/use-search-params + */ +export default function useSearchParams(defaultInit) { + const defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)) + const hasSetSearchParamsRef = React.useRef(false) + + const searchParams = React.useMemo( + () => + getSearchParamsForLocation( + location.search, + hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current + ), + [location.search] + ) + + const defaultOptions = { + replace: false, + state: null, + preventScrollReset: false + } + + const setSearchParams = React.useCallback( + (nextInit, { replace, state, preventScrollReset } = defaultOptions) => { + const newSearchParams = createSearchParams( + typeof nextInit === 'function' ? nextInit(searchParams) : nextInit + ) + hasSetSearchParamsRef.current = true + if (replace) { + history.replaceState(state, '', '?' + newSearchParams) + } else { + history.pushState(state, '', '?' + newSearchParams) + } + if (!preventScrollReset) { + window.scrollTo(0, 0) + } + }, + [searchParams] + ) + + return [searchParams, setSearchParams] +} From 3c37ae5e5f793f7feda14e1068fa5037e6020913 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:04:28 +0900 Subject: [PATCH 02/32] =?UTF-8?q?clsx=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 154307d796e..bd10ad6f50f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "autosize": "^4.0.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "choices.js": "^10.1.0", + "clsx": "^2.0.0", "css-loader": "^5.0.1", "dayjs": "^1.10.4", "escape-html": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 2f8457ae206..a51c791ca01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2501,6 +2501,11 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +clsx@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" From 5d28d4ece89fba51272a4bf339b57f72146c33cd Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:04:41 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=E9=83=BD=E9=81=93=E5=BA=9C=E7=9C=8C?= =?UTF-8?q?=E5=88=A5=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E4=B8=80=E8=A6=A7?= =?UTF-8?q?=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/users/areas_controller.rb | 8 ++ app/controllers/users/areas_controller.rb | 7 ++ app/javascript/components/Areas/Areas.jsx | 91 +++++++++++++++++++ app/javascript/components/ui/EmptyMessage.jsx | 14 +++ app/javascript/components/ui/UserGroup.jsx | 47 ++++++++++ app/models/area.rb | 80 ++++++++++++++++ app/models/user.rb | 1 + app/views/api/users/areas/index.json.jbuilder | 3 + app/views/users/_lg_page_tabs.html.slim | 3 + app/views/users/areas/index.html.slim | 19 ++++ config/routes/api.rb | 1 + config/routes/users.rb | 1 + db/fixtures/users.yml | 8 ++ 13 files changed, 283 insertions(+) create mode 100644 app/controllers/api/users/areas_controller.rb create mode 100644 app/controllers/users/areas_controller.rb create mode 100644 app/javascript/components/Areas/Areas.jsx create mode 100644 app/javascript/components/ui/EmptyMessage.jsx create mode 100644 app/javascript/components/ui/UserGroup.jsx create mode 100644 app/models/area.rb create mode 100644 app/views/api/users/areas/index.json.jbuilder create mode 100644 app/views/users/areas/index.html.slim diff --git a/app/controllers/api/users/areas_controller.rb b/app/controllers/api/users/areas_controller.rb new file mode 100644 index 00000000000..274bde64f52 --- /dev/null +++ b/app/controllers/api/users/areas_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class API::Users::AreasController < API::BaseController + def index + # params[:area]がnilの場合は東京都を取得 + @users = Area.users(params[:area] || '13', params[:region]) + end +end diff --git a/app/controllers/users/areas_controller.rb b/app/controllers/users/areas_controller.rb new file mode 100644 index 00000000000..f14ee6d8c1e --- /dev/null +++ b/app/controllers/users/areas_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Users::AreasController < ApplicationController + def index + @user_counts = Area.user_counts_by_subdivision + end +end diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx new file mode 100644 index 00000000000..ccf422a4ba9 --- /dev/null +++ b/app/javascript/components/Areas/Areas.jsx @@ -0,0 +1,91 @@ +import React from 'react' +import LoadingListPlaceholder from '../LoadingListPlaceholder' +import EmptyMessage from '../ui/EmptyMessage' +import { UserGroup, UserGroupHeader, UserGroupIcons } from '../ui/UserGroup' +import useSWR from 'swr' +import fetcher from '../../fetcher' +import useSearchParams from '../../hooks/useSearchParams' + +function Region({ region, areas, handleClick }) { + return ( +
  • +

    {region}

    +
      + {Object.keys(areas).map(area => ( +
    • + +
    • + ))} +
    +
  • + ) +} + +export default function Areas({ userCounts }) { + const [searchParams, setSearchParams] = useSearchParams({ area: '東京都' }) + const apiUrl = '/api/users/areas?' + const { data: users, error, mutate } = useSWR(apiUrl + searchParams, fetcher) + + const handleClick = async (region, area) => { + const searchParams = new URLSearchParams({ region, area }) + const newUsers = await fetcher(`${apiUrl}${searchParams}`) + .catch((error) => { + console.error(error) + }) + mutate(newUsers) + setSearchParams({ region, area }) + } + + const onPopstate = async () => { + const search = new URL(location).searchParams + const newUsers = await fetcher(`${apiUrl}${search}`) + .catch((error) => { + console.error(error) + }) + mutate(newUsers) + } + + React.useEffect(() => { + window.addEventListener('popstate', onPopstate) + return () => window.removeEventListener('popstate', onPopstate) + }, [onPopstate]) + + if (error) return console.warn(error) + if (!users) { + return ( +
    +
    + +
    +
    + ) + } + + return ( +
    +
    +
      + {Object.keys(userCounts).map(region => ( + + ))} +
    +
    + {users.length > 0 + ? + { searchParams.get('area') } + + + : 都道府県別ユーザー一覧はありません + } +
    +
    +
    + ) +} diff --git a/app/javascript/components/ui/EmptyMessage.jsx b/app/javascript/components/ui/EmptyMessage.jsx new file mode 100644 index 00000000000..818912ed5cb --- /dev/null +++ b/app/javascript/components/ui/EmptyMessage.jsx @@ -0,0 +1,14 @@ +import React from 'react' + +export default function EmptyMessage({ children }) { + return ( +
    +
    + +
    +

    + { children } +

    +
    + ) +} diff --git a/app/javascript/components/ui/UserGroup.jsx b/app/javascript/components/ui/UserGroup.jsx new file mode 100644 index 00000000000..0a82e8ce181 --- /dev/null +++ b/app/javascript/components/ui/UserGroup.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import clsx from 'clsx' + +const UserGroup = ({ className, ...props }) => { + return ( +
    + ) +} + +const UserGroupHeader = ({ className, children, ...props }) => { + return ( +
    +

    {children}

    +
    + ) +} + +const UserGroupIcons = ({ users, className, ...props }) => { + return ( +
    +
    + {users.map((user) => ( + + ))} +
    +
    + ) +} + +const UserIcon = ({ user }) => { + const userRole = `is-${user.primary_role}` + + return ( + + + + + + ) +} + +export { UserGroup, UserGroupHeader, UserGroupIcons } diff --git a/app/models/area.rb b/app/models/area.rb new file mode 100644 index 00000000000..73933549843 --- /dev/null +++ b/app/models/area.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class Area + HOKKAIDO_TOHOKU = %w[北海道 青森県 岩手県 宮城県 秋田県 山形県 福島県].freeze + KANTO = %w[茨城県 栃木県 群馬県 埼玉県 千葉県 東京都 神奈川県].freeze + CHUBU = %w[新潟県 富山県 石川県 福井県 山梨県 長野県 岐阜県 静岡県 愛知県].freeze + KINKI = %w[三重県 滋賀県 京都府 大阪府 兵庫県 奈良県 和歌山県].freeze + CHUGOKU = %w[鳥取県 島根県 岡山県 広島県 山口県].freeze + SHIKOKU = %w[徳島県 香川県 愛媛県 高知県].freeze + KYUSHU_OKINAWA = %w[福岡県 佐賀県 長崎県 熊本県 大分県 宮崎県 鹿児島県 沖縄県].freeze + REGIONS = { + '北海道・東北地方' => HOKKAIDO_TOHOKU, + '関東地方' => KANTO, + '中部地方' => CHUBU, + '近畿地方' => KINKI, + '中国地方' => CHUGOKU, + '四国地方' => SHIKOKU, + '九州・沖縄地方' => KYUSHU_OKINAWA + }.freeze + JP = ISO3166::Country[:JP] + + class << self + # 日本の場合は都道府県、海外の場合は国名によってユーザーを取得 + def users(subdivision_or_country, region) + if region == '海外' + country = ISO3166::Country.find_country_by_any_name(subdivision_or_country) + User + .with_attached_avatar + .where(country_code: country.alpha2) + else + subdivision_code = JP.find_subdivision_by_name(subdivision_or_country).code + User + .with_attached_avatar + .where(subdivision_code: subdivision_code.to_s) + end + end + + def user_counts_by_subdivision + translated_pairs = to_jp(country_subdivision_pairs) + by_countries = translated_pairs.group_by(&:first) + # hash autovivification https://stackoverflow.com/questions/50468234/better-way-to-initialize-and-update-deeply-nested-hash + by_countries.each_with_object(Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }) do |v, result| + country, pair_array = v + if country == '日本' + pair_array.each do |_, s| + result = select_region(s, result) + end + else + result['海外'][pair_array.map(&:first)[0]] = pair_array.map(&:second).length + end + end + end + + private + + def country_subdivision_pairs + User + .select('country_code, subdivision_code') + .where.not(subdivision_code: nil) + .pluck(:country_code, :subdivision_code) + end + + def to_jp(country_subdivision_pairs) + country_subdivision_pairs.map do |country_code, subdivision_code| + country = ISO3166::Country[country_code] + subdivision = country.subdivisions[subdivision_code] + [country.translations[I18n.locale.to_s], subdivision.translations[I18n.locale.to_s]] + end + end + + def select_region(subdivision, result) + REGIONS.each do |region_name, region_array| + if region_array.include?(subdivision) + result[region_name][subdivision] = result[region_name][subdivision].is_a?(Numeric) ? result[region_name][subdivision] + 1 : 1 + end + end + result + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 8d1367ac7e6..2369e07e165 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -374,6 +374,7 @@ class User < ApplicationRecord .where('completed_at <= ?', 2.weeks.ago.end_of_day) } scope :campaign, -> { where(created_at: Campaign.recently_campaign) } + columns_for_keyword_search( :login_name, :name, diff --git a/app/views/api/users/areas/index.json.jbuilder b/app/views/api/users/areas/index.json.jbuilder new file mode 100644 index 00000000000..19130de305c --- /dev/null +++ b/app/views/api/users/areas/index.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.array! @users, partial: "api/users/user", as: :user diff --git a/app/views/users/_lg_page_tabs.html.slim b/app/views/users/_lg_page_tabs.html.slim index 15bc69cf574..24357be2558 100644 --- a/app/views/users/_lg_page_tabs.html.slim +++ b/app/views/users/_lg_page_tabs.html.slim @@ -16,3 +16,6 @@ li.page-tabs__item = link_to users_companies_path, class: "page-tabs__item-link #{users_current_page_tab_or_not('companies')}" do | 企業別 + li.page-tabs__item + = link_to users_areas_path, class: "page-tabs__item-link #{users_current_page_tab_or_not('areas')}" do + | 都道府県別 diff --git a/app/views/users/areas/index.html.slim b/app/views/users/areas/index.html.slim new file mode 100644 index 00000000000..c37f5a6e630 --- /dev/null +++ b/app/views/users/areas/index.html.slim @@ -0,0 +1,19 @@ +- title '都道府県別ユーザー一覧' +- set_meta_tags description: '都道府県別ユーザー一覧ページです。' + +header.page-header + .container + .page-header__inner + h2.page-header__title ユーザー一覧 + .page-header-actions + ul.page-header-actions__items + += render '/users/lg_page_tabs' + +main.page-main + header.page-main-header + .container + .page-main-header__inner + h1.page-main-header__title 都道府県別 + hr.a-border + = react_component('Areas/Areas', userCounts: @user_counts) diff --git a/config/routes/api.rb b/config/routes/api.rb index 03b193cf398..6b29942dee0 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -33,6 +33,7 @@ resources :checks, only: %i(index create destroy) resources :mention_users, only: %i(index) namespace :users do + resources :areas, only: %i(index) resources :companies, only: %i(index) resources :worried, only: %i(index) end diff --git a/config/routes/users.rb b/config/routes/users.rb index 30bd9ed6d9d..f3a6e1a17f8 100644 --- a/config/routes/users.rb +++ b/config/routes/users.rb @@ -5,6 +5,7 @@ namespace :users do get "tags", to: "tags#index" resources :companies, only: %i(index) + resources :areas, only: %i(index) end resources :users, only: %i(index show new create) do diff --git a/db/fixtures/users.yml b/db/fixtures/users.yml index 073a04284fa..078d733cd5a 100644 --- a/db/fixtures/users.yml +++ b/db/fixtures/users.yml @@ -1018,6 +1018,8 @@ neverlogin: # 1度もログインしたことがないユーザー course: course1 os: mac experience: rails + country_code: US + subdivision_code: HI free: true updated_at: "2022-07-11 00:00:00" created_at: "2022-07-11 00:00:00" @@ -1206,6 +1208,8 @@ sotsugyoukigyoshozoku: course: course1 os: mac experience: rails + country_code: CA + subdivision_code: QC graduated_on: "2022-07-04" unsubscribe_email_token: YnhyqxqalslEkTCM2jyVwg sad_streak: true @@ -1228,6 +1232,8 @@ advisernocolleguetrainee: description: '同僚がいないアドバイザーです。' adviser: true course: course1 + country_code: JP + subdivision_code: '42' unsubscribe_email_token: 6ULb9vj1AJzTH_mOsfR46Q updated_at: "2014-01-01 00:00:04" created_at: "2014-01-01 00:00:04" @@ -1246,6 +1252,8 @@ nagai-kyuukai: job: office_worker os: mac experience: inexperienced + country_code: JP + subdivision_code: '09' hibernated_at: <%= Time.current - 6.months %> updated_at: "2014-01-01 00:00:13" created_at: "2014-01-01 00:00:13" From b41d075b1803dac8bee0dde411a2d5df98ff26b5 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:30:26 +0900 Subject: [PATCH 04/32] run prettier --- app/javascript/components/Areas/Areas.jsx | 35 ++++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index ccf422a4ba9..da887844357 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -11,10 +11,10 @@ function Region({ region, areas, handleClick }) {
  • {region}

      - {Object.keys(areas).map(area => ( + {Object.keys(areas).map((area) => (
    • ))} @@ -30,20 +30,20 @@ export default function Areas({ userCounts }) { const handleClick = async (region, area) => { const searchParams = new URLSearchParams({ region, area }) - const newUsers = await fetcher(`${apiUrl}${searchParams}`) - .catch((error) => { + const newUsers = await fetcher(apiUrl + searchParams).catch( + (error) => { console.error(error) - }) + } + ) mutate(newUsers) setSearchParams({ region, area }) } const onPopstate = async () => { const search = new URL(location).searchParams - const newUsers = await fetcher(`${apiUrl}${search}`) - .catch((error) => { - console.error(error) - }) + const newUsers = await fetcher(apiUrl + search).catch((error) => { + console.error(error) + }) mutate(newUsers) } @@ -67,7 +67,7 @@ export default function Areas({ userCounts }) {
        - {Object.keys(userCounts).map(region => ( + {Object.keys(userCounts).map((region) => (
        - {users.length > 0 - ? - { searchParams.get('area') } - - - : 都道府県別ユーザー一覧はありません - } + {users.length > 0 ? ( + + {searchParams.get('area')} + + + ) : ( + 都道府県別ユーザー一覧はありません + )}
      From e93cb0fff49b53369c9f42f400bcdff51499886f Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:36:07 +0900 Subject: [PATCH 05/32] =?UTF-8?q?prettier=E3=81=AE=E5=AE=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/ui/EmptyMessage.jsx | 4 +--- app/javascript/components/ui/UserGroup.jsx | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/javascript/components/ui/EmptyMessage.jsx b/app/javascript/components/ui/EmptyMessage.jsx index 818912ed5cb..1e596d05e37 100644 --- a/app/javascript/components/ui/EmptyMessage.jsx +++ b/app/javascript/components/ui/EmptyMessage.jsx @@ -6,9 +6,7 @@ export default function EmptyMessage({ children }) {
      -

      - { children } -

      +

      {children}

  • ) } diff --git a/app/javascript/components/ui/UserGroup.jsx b/app/javascript/components/ui/UserGroup.jsx index 0a82e8ce181..4489113aeeb 100644 --- a/app/javascript/components/ui/UserGroup.jsx +++ b/app/javascript/components/ui/UserGroup.jsx @@ -2,15 +2,13 @@ import React from 'react' import clsx from 'clsx' const UserGroup = ({ className, ...props }) => { - return ( -
    - ) + return
    } const UserGroupHeader = ({ className, children, ...props }) => { return (
    -

    {children}

    +

    {children}

    ) } From 9b807c83ce10c5b874b5a83e85385869cb84fd99 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:37:25 +0900 Subject: [PATCH 06/32] =?UTF-8?q?prettier=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index da887844357..59ab2f23358 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -30,11 +30,9 @@ export default function Areas({ userCounts }) { const handleClick = async (region, area) => { const searchParams = new URLSearchParams({ region, area }) - const newUsers = await fetcher(apiUrl + searchParams).catch( - (error) => { - console.error(error) - } - ) + const newUsers = await fetcher(apiUrl + searchParams).catch((error) => { + console.error(error) + }) mutate(newUsers) setSearchParams({ region, area }) } From 4237531f172734b4e4066d1deef96b6dd669ae67 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 00:52:02 +0900 Subject: [PATCH 07/32] =?UTF-8?q?section=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 59ab2f23358..6838ddb2807 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -63,18 +63,20 @@ export default function Areas({ userCounts }) { return (
    -
    -
      - {Object.keys(userCounts).map((region) => ( - - ))} -
    -
    +
    +
    +
      + {Object.keys(userCounts).map((region) => ( + + ))} +
    +
    +
    {users.length > 0 ? ( {searchParams.get('area')} @@ -83,7 +85,7 @@ export default function Areas({ userCounts }) { ) : ( 都道府県別ユーザー一覧はありません )} -
    +
    ) From baa1c1060894dc527558481b470ef3cabc71dd68 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 01:40:57 +0900 Subject: [PATCH 08/32] =?UTF-8?q?=E5=A4=89=E6=95=B0=E5=90=8D=E3=82=92prima?= =?UTF-8?q?ryRole=E3=81=B8=E3=81=A8=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/ui/UserGroup.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/ui/UserGroup.jsx b/app/javascript/components/ui/UserGroup.jsx index 4489113aeeb..7021ec98273 100644 --- a/app/javascript/components/ui/UserGroup.jsx +++ b/app/javascript/components/ui/UserGroup.jsx @@ -26,11 +26,11 @@ const UserGroupIcons = ({ users, className, ...props }) => { } const UserIcon = ({ user }) => { - const userRole = `is-${user.primary_role}` + const primaryRole = `is-${user.primary_role}` return ( - + Date: Sun, 8 Oct 2023 01:49:32 +0900 Subject: [PATCH 09/32] =?UTF-8?q?user=E3=83=A2=E3=83=87=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E4=BD=99=E8=A8=88=E3=81=AA=E5=A4=89=E6=9B=B4=E3=82=92=E5=8F=96?= =?UTF-8?q?=E3=82=8A=E6=B6=88=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/user.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 2369e07e165..8d1367ac7e6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -374,7 +374,6 @@ class User < ApplicationRecord .where('completed_at <= ?', 2.weeks.ago.end_of_day) } scope :campaign, -> { where(created_at: Campaign.recently_campaign) } - columns_for_keyword_search( :login_name, :name, From 48227205f447bed8086cab6b62889db0c8cfc09e Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Sun, 8 Oct 2023 03:22:30 +0900 Subject: [PATCH 10/32] =?UTF-8?q?UserGroup=E3=81=A0=E3=81=91export?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 6 +++--- app/javascript/components/ui/UserGroup.jsx | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 6838ddb2807..89b7f068c7d 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -1,7 +1,7 @@ import React from 'react' import LoadingListPlaceholder from '../LoadingListPlaceholder' import EmptyMessage from '../ui/EmptyMessage' -import { UserGroup, UserGroupHeader, UserGroupIcons } from '../ui/UserGroup' +import { UserGroup } from '../ui/UserGroup' import useSWR from 'swr' import fetcher from '../../fetcher' import useSearchParams from '../../hooks/useSearchParams' @@ -79,8 +79,8 @@ export default function Areas({ userCounts }) {
    {users.length > 0 ? ( - {searchParams.get('area')} - + {searchParams.get('area')} + ) : ( 都道府県別ユーザー一覧はありません diff --git a/app/javascript/components/ui/UserGroup.jsx b/app/javascript/components/ui/UserGroup.jsx index 7021ec98273..cc86d9687eb 100644 --- a/app/javascript/components/ui/UserGroup.jsx +++ b/app/javascript/components/ui/UserGroup.jsx @@ -1,7 +1,7 @@ import React from 'react' import clsx from 'clsx' -const UserGroup = ({ className, ...props }) => { +export const UserGroup = ({ className, ...props }) => { return
    } @@ -42,4 +42,5 @@ const UserIcon = ({ user }) => { ) } -export { UserGroup, UserGroupHeader, UserGroupIcons } +UserGroup.Header = UserGroupHeader +UserGroup.Icons = UserGroupIcons From 0307ab62272c9e0e2a32194b31b21baf614232bf Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Thu, 12 Oct 2023 12:21:13 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=E5=88=86=E3=81=8B=E3=82=8A=E3=81=AB?= =?UTF-8?q?=E3=81=8F=E3=81=84=E5=A4=89=E6=95=B0=E5=90=8D=E3=82=84=E9=96=A2?= =?UTF-8?q?=E6=95=B0=E5=90=8D=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/users/areas_controller.rb | 2 +- app/javascript/components/Areas/Areas.jsx | 22 +++++++++++----------- app/models/area.rb | 11 +++++------ app/views/users/areas/index.html.slim | 2 +- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/app/controllers/users/areas_controller.rb b/app/controllers/users/areas_controller.rb index f14ee6d8c1e..1afd8eb0e16 100644 --- a/app/controllers/users/areas_controller.rb +++ b/app/controllers/users/areas_controller.rb @@ -2,6 +2,6 @@ class Users::AreasController < ApplicationController def index - @user_counts = Area.user_counts_by_subdivision + @number_of_users = Area.number_of_users end end diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 89b7f068c7d..e6fd53b4fd6 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -6,15 +6,15 @@ import useSWR from 'swr' import fetcher from '../../fetcher' import useSearchParams from '../../hooks/useSearchParams' -function Region({ region, areas, handleClick }) { +function Region({ region, numberOfUsersByRegion, handleClick }) { return (
  • {region}

      - {Object.keys(areas).map((area) => ( + {Object.keys(numberOfUsersByRegion).map((area) => (
    • ))} @@ -23,18 +23,18 @@ function Region({ region, areas, handleClick }) { ) } -export default function Areas({ userCounts }) { +export default function Areas({ numberOfUsers }) { const [searchParams, setSearchParams] = useSearchParams({ area: '東京都' }) const apiUrl = '/api/users/areas?' const { data: users, error, mutate } = useSWR(apiUrl + searchParams, fetcher) const handleClick = async (region, area) => { - const searchParams = new URLSearchParams({ region, area }) - const newUsers = await fetcher(apiUrl + searchParams).catch((error) => { + const search = new URLSearchParams({ region, area }) + const newUsers = await fetcher(apiUrl + search).catch((error) => { console.error(error) }) mutate(newUsers) - setSearchParams({ region, area }) + setSearchParams(search) } const onPopstate = async () => { @@ -62,15 +62,15 @@ export default function Areas({ userCounts }) { } return ( -
      +
        - {Object.keys(userCounts).map((region) => ( + {Object.keys(numberOfUsers).map((region) => ( ))} @@ -79,7 +79,7 @@ export default function Areas({ userCounts }) {
        {users.length > 0 ? ( - {searchParams.get('area')} + {searchParams.get('area') || '東京都'} ) : ( diff --git a/app/models/area.rb b/app/models/area.rb index 73933549843..25a7421150a 100644 --- a/app/models/area.rb +++ b/app/models/area.rb @@ -17,7 +17,6 @@ class Area '四国地方' => SHIKOKU, '九州・沖縄地方' => KYUSHU_OKINAWA }.freeze - JP = ISO3166::Country[:JP] class << self # 日本の場合は都道府県、海外の場合は国名によってユーザーを取得 @@ -28,15 +27,15 @@ def users(subdivision_or_country, region) .with_attached_avatar .where(country_code: country.alpha2) else - subdivision_code = JP.find_subdivision_by_name(subdivision_or_country).code + subdivision_code = ISO3166::Country[:JP].find_subdivision_by_name(subdivision_or_country).code User .with_attached_avatar .where(subdivision_code: subdivision_code.to_s) end end - def user_counts_by_subdivision - translated_pairs = to_jp(country_subdivision_pairs) + def number_of_users + translated_pairs = translate(country_subdivision_pairs) by_countries = translated_pairs.group_by(&:first) # hash autovivification https://stackoverflow.com/questions/50468234/better-way-to-initialize-and-update-deeply-nested-hash by_countries.each_with_object(Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }) do |v, result| @@ -60,11 +59,11 @@ def country_subdivision_pairs .pluck(:country_code, :subdivision_code) end - def to_jp(country_subdivision_pairs) + def translate(country_subdivision_pairs) country_subdivision_pairs.map do |country_code, subdivision_code| country = ISO3166::Country[country_code] subdivision = country.subdivisions[subdivision_code] - [country.translations[I18n.locale.to_s], subdivision.translations[I18n.locale.to_s]] + [country.translations['ja'], subdivision.translations['ja']] end end diff --git a/app/views/users/areas/index.html.slim b/app/views/users/areas/index.html.slim index c37f5a6e630..150c01d6e91 100644 --- a/app/views/users/areas/index.html.slim +++ b/app/views/users/areas/index.html.slim @@ -16,4 +16,4 @@ main.page-main .page-main-header__inner h1.page-main-header__title 都道府県別 hr.a-border - = react_component('Areas/Areas', userCounts: @user_counts) + = react_component('Areas/Areas', numberOfUsers: @number_of_users) From 79c810837908724a49c99ccbfde2d4daffdb37d4 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Thu, 12 Oct 2023 12:21:48 +0900 Subject: [PATCH 12/32] =?UTF-8?q?=E7=B0=A1=E6=98=93=E3=81=AA=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/fixtures/users.yml | 8 ++++++++ test/models/area_test.rb | 18 ++++++++++++++++++ test/system/user/areas_test.rb | 14 ++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 test/models/area_test.rb create mode 100644 test/system/user/areas_test.rb diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 2d7092ff104..39a76ce828a 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -818,6 +818,8 @@ neverlogin: # 1度もログインしたことがないユーザー course: course1 os: mac experience: rails + country_code: US + subdivision_code: HI free: true updated_at: "2022-07-11 00:00:00" created_at: "2022-07-11 00:00:00" @@ -840,6 +842,8 @@ kyuukai: job: office_worker os: mac experience: inexperienced + country_code: JP + subdivision_code: '09' github_account: kyuukai unsubscribe_email_token: k3a49_NwgTsiJS0oHGU2Fw hibernated_at: "2020-01-01 00:00:00" @@ -860,6 +864,8 @@ sotsugyoukigyoshozoku: course: course1 os: mac experience: rails + country_code: CA + subdivision_code: QC graduated_on: "2022-07-04" unsubscribe_email_token: YnhyqxqalslEkTCM2jyVwg sad_streak: true @@ -875,6 +881,8 @@ advisernocolleguetrainee: name: あどばいざ 同僚いない子 name_kana: アドバイザ ドウリョウイナイコ course: course1 + country_code: JP + subdivision_code: '42' job: office_worker os: mac company: company26 diff --git a/test/models/area_test.rb b/test/models/area_test.rb new file mode 100644 index 00000000000..5ee4ec972fb --- /dev/null +++ b/test/models/area_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AreaTest < ActiveSupport::TestCase + test '#users' do + assert_includes Area.users('米国', '海外'), users(:tom) + assert_includes Area.users('東京都', '関東地方'), users(:kimura) + end + + test '#number_of_users' do + assert_equal Area.number_of_users, { + '関東地方' => { '東京都' => 1, '栃木県' => 1 }, + '九州・沖縄地方' => { '長崎県' => 1 }, + '海外' => { '米国' => 2, 'カナダ' => 1 } + } + end +end diff --git a/test/system/user/areas_test.rb b/test/system/user/areas_test.rb new file mode 100644 index 00000000000..52e0f023022 --- /dev/null +++ b/test/system/user/areas_test.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'application_system_test_case' + +class User::AreasTest < ApplicationSystemTestCase + test 'show users devided by areas' do + visit_with_auth '/users/areas', 'komagata' + assert_equal '都道府県別ユーザー一覧 | FBC', title + within "[data-testid='areas']" do + assert_text '関東地方' + assert_selector "[data-login-name='kimura']" + end + end +end From 197284a27d95748a86366acb62c95688fa8706f8 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Thu, 12 Oct 2023 13:49:39 +0900 Subject: [PATCH 13/32] =?UTF-8?q?prettier=E3=82=92=E5=AE=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index e6fd53b4fd6..7d621ab694d 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -62,7 +62,7 @@ export default function Areas({ numberOfUsers }) { } return ( -
        +
          @@ -79,7 +79,9 @@ export default function Areas({ numberOfUsers }) {
          {users.length > 0 ? ( - {searchParams.get('area') || '東京都'} + + {searchParams.get('area') || '東京都'} + ) : ( From fdcfc87a31d07a058a817254d5b78659f188d068 Mon Sep 17 00:00:00 2001 From: Koji Ishimoto Date: Fri, 20 Oct 2023 10:25:00 +0900 Subject: [PATCH 14/32] =?UTF-8?q?usePopstate=E3=82=92=E4=BD=9C=E3=81=A3?= =?UTF-8?q?=E3=81=A6=E4=BD=BF=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 11 ++----- app/javascript/hooks/useSearchParams.js | 35 +++++++++++++++++++---- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 7d621ab694d..2e9aefa8983 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -4,7 +4,7 @@ import EmptyMessage from '../ui/EmptyMessage' import { UserGroup } from '../ui/UserGroup' import useSWR from 'swr' import fetcher from '../../fetcher' -import useSearchParams from '../../hooks/useSearchParams' +import { useSearchParams, usePopstate } from '../../hooks/useSearchParams' function Region({ region, numberOfUsersByRegion, handleClick }) { return ( @@ -37,18 +37,13 @@ export default function Areas({ numberOfUsers }) { setSearchParams(search) } - const onPopstate = async () => { + usePopstate(async () => { const search = new URL(location).searchParams const newUsers = await fetcher(apiUrl + search).catch((error) => { console.error(error) }) mutate(newUsers) - } - - React.useEffect(() => { - window.addEventListener('popstate', onPopstate) - return () => window.removeEventListener('popstate', onPopstate) - }, [onPopstate]) + }) if (error) return console.warn(error) if (!users) { diff --git a/app/javascript/hooks/useSearchParams.js b/app/javascript/hooks/useSearchParams.js index 2d64fb5bf95..b204a75a3ff 100644 --- a/app/javascript/hooks/useSearchParams.js +++ b/app/javascript/hooks/useSearchParams.js @@ -1,4 +1,4 @@ -import React from 'react' +import { useRef, useEffect, useCallback, useMemo } from 'react' function createSearchParams(init = '') { return new URLSearchParams( @@ -32,13 +32,14 @@ function getSearchParamsForLocation(locationSearch, defaultSearchParams) { } /** + * React RouterのuseSearchParamsと同じように働くので参考にしてください * @see https://reactrouter.com/en/main/hooks/use-search-params */ -export default function useSearchParams(defaultInit) { - const defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit)) - const hasSetSearchParamsRef = React.useRef(false) +function useSearchParams(defaultInit) { + const defaultSearchParamsRef = useRef(createSearchParams(defaultInit)) + const hasSetSearchParamsRef = useRef(false) - const searchParams = React.useMemo( + const searchParams = useMemo( () => getSearchParamsForLocation( location.search, @@ -53,7 +54,7 @@ export default function useSearchParams(defaultInit) { preventScrollReset: false } - const setSearchParams = React.useCallback( + const setSearchParams = useCallback( (nextInit, { replace, state, preventScrollReset } = defaultOptions) => { const newSearchParams = createSearchParams( typeof nextInit === 'function' ? nextInit(searchParams) : nextInit @@ -73,3 +74,25 @@ export default function useSearchParams(defaultInit) { return [searchParams, setSearchParams] } + +/** + * @see https://usehooks-ts.com/react-hook/use-event-listener + */ +function usePopstate(handler) { + const savedHandler = useRef(handler) + + useEffect(() => { + savedHandler.current = handler + }, [handler]) + + useEffect(() => { + if (!(window && window.addEventListener)) return + const listener = (event) => savedHandler.current(event) + window.addEventListener('popstate', listener) + return () => { + window.removeEventListener('popstate', listener) + } + }, []) +} + +export { useSearchParams, usePopstate } From ecf1ba63b514c76b889b7ec942f7005a53df787b Mon Sep 17 00:00:00 2001 From: machida Date: Tue, 24 Oct 2023 16:05:54 +0900 Subject: [PATCH 15/32] =?UTF-8?q?=E9=83=BD=E9=81=93=E5=BA=9C=E7=9C=8C?= =?UTF-8?q?=E5=88=A5=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E4=B8=80=E8=A6=A7?= =?UTF-8?q?=E3=81=AE=E5=A4=A7=E6=9E=A0=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 64 ++++++++++--------- .../stylesheets/_common-imports.sass | 2 + .../shared/blocks/_side-nav-block.sass | 3 + 3 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 app/javascript/stylesheets/shared/blocks/_side-nav-block.sass diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 2e9aefa8983..718e3f22565 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -8,12 +8,12 @@ import { useSearchParams, usePopstate } from '../../hooks/useSearchParams' function Region({ region, numberOfUsersByRegion, handleClick }) { return ( -
        • -

          {region}

          -
            +
          • +

            {region}

            +
              {Object.keys(numberOfUsersByRegion).map((area) => ( -
            • -
            • @@ -59,30 +59,36 @@ export default function Areas({ numberOfUsers }) { return (
              -
              -
                - {Object.keys(numberOfUsers).map((region) => ( - - ))} -
              -
              -
              - {users.length > 0 ? ( - - - {searchParams.get('area') || '東京都'} - - - - ) : ( - 都道府県別ユーザー一覧はありません - )} -
              +
              +
              + +
              +
              +
              + {users.length > 0 ? ( + + + {searchParams.get('area') || '東京都'} + + + + ) : ( + 都道府県別ユーザー一覧はありません + )} +
              +
              +
              ) diff --git a/app/javascript/stylesheets/_common-imports.sass b/app/javascript/stylesheets/_common-imports.sass index cd13d66ce8b..5b8cf95a946 100644 --- a/app/javascript/stylesheets/_common-imports.sass +++ b/app/javascript/stylesheets/_common-imports.sass @@ -173,4 +173,6 @@ @import "shared/blocks/card-list/card-list" @import "shared/blocks/card-list/products" +@import "shared/blocks/side-nav-block" + @import "shared/helpers/state" diff --git a/app/javascript/stylesheets/shared/blocks/_side-nav-block.sass b/app/javascript/stylesheets/shared/blocks/_side-nav-block.sass new file mode 100644 index 00000000000..bbea9a55f09 --- /dev/null +++ b/app/javascript/stylesheets/shared/blocks/_side-nav-block.sass @@ -0,0 +1,3 @@ +.side-nav-block + .side-nav-block + & + margin-top: 2rem From ff5f81e73eafaed1afce5b20fff8f3c884f69629 Mon Sep 17 00:00:00 2001 From: machida Date: Wed, 1 Nov 2023 02:12:42 +0900 Subject: [PATCH 16/32] =?UTF-8?q?page-nav=20=E3=81=AE=20css=20=E3=82=92?= =?UTF-8?q?=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/components/Areas/Areas.jsx | 31 ++++++++------ app/javascript/courses-practices.vue | 2 +- app/javascript/elapsed_days.vue | 2 +- ...ing-courses-practices-page-placeholder.vue | 2 +- .../blocks/header/_header-dropdown.sass | 13 +++--- .../application/blocks/page/_page-body.sass | 10 +++++ .../application/blocks/tags/_random-tags.sass | 12 +++--- .../stylesheets/config/variables/_layout.sass | 2 +- .../stylesheets/shared/blocks/_page-nav.sass | 41 +++++-------------- app/views/practices/show.html.slim | 5 ++- app/views/questions/_nav_questions.html.slim | 32 +++++++-------- 11 files changed, 76 insertions(+), 76 deletions(-) diff --git a/app/javascript/components/Areas/Areas.jsx b/app/javascript/components/Areas/Areas.jsx index 718e3f22565..82bb7e8231c 100644 --- a/app/javascript/components/Areas/Areas.jsx +++ b/app/javascript/components/Areas/Areas.jsx @@ -8,17 +8,24 @@ import { useSearchParams, usePopstate } from '../../hooks/useSearchParams' function Region({ region, numberOfUsersByRegion, handleClick }) { return ( -
            • -

              {region}

              -
                - {Object.keys(numberOfUsersByRegion).map((area) => ( -
              • - -
              • - ))} -
              +
            • +
              + +
              +
                + {Object.keys(numberOfUsersByRegion).map((area) => ( +
              • + +
              • + ))} +
              +
            • ) } @@ -59,7 +66,7 @@ export default function Areas({ numberOfUsers }) { return (
              -
              +