@hitkey-io/strapi-plugin-region 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikolay Larsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ <p align="center">
2
+ <img src="docs/logo.png" alt="Region Select" width="120" />
3
+ </p>
4
+
5
+ # Strapi Plugin Region
6
+
7
+ Cascading **Country → Region** selector as a Strapi 5 custom field. Pick a country, then pick a region — stored as structured JSON, auto-parsed in API responses.
8
+
9
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
10
+ [![Strapi 5](https://img.shields.io/badge/strapi-v5.x-blueviolet.svg)](https://strapi.io)
11
+
12
+ <p align="center">
13
+ <img src="docs/screenshots/02-region-selector.jpeg" alt="Country and Region selector with autocomplete" width="720" />
14
+ </p>
15
+
16
+ ## Features
17
+
18
+ - Two-stage cascading selector: **Country → Region** with autocomplete
19
+ - Country filtering: show **all**, **only selected**, or **all except selected** countries
20
+ - ISO 3166-1 alpha-2 country codes with full country and region datasets
21
+ - Stored as a single `string` field — no extra tables or relations
22
+ - Automatic JSON parsing in API responses (Koa middleware)
23
+ - Formatted **"Country, Region"** column in Content Manager list view
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install @hitkey-io/strapi-plugin-region
29
+ ```
30
+
31
+ Or with yarn:
32
+
33
+ ```bash
34
+ yarn add @hitkey-io/strapi-plugin-region
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ <p align="center">
40
+ <img src="docs/screenshots/01-field-options.png" alt="Field options in Content-Type Builder" width="720" />
41
+ </p>
42
+
43
+ ### Adding the field
44
+
45
+ 1. Open the **Content-Type Builder**
46
+ 2. Add a new **Custom** field → **Region Select**
47
+ 3. Configure country filtering:
48
+
49
+ | Option | Description |
50
+ |--------|-------------|
51
+ | **All countries** | Show every country in the selector (default) |
52
+ | **Only selected** | Show only the listed country codes |
53
+ | **All except selected** | Show all countries except the listed ones |
54
+
55
+ Country codes are entered one per line in ISO alpha-2 format (e.g. `US`, `TR`, `AE`).
56
+
57
+ ### Plugin config
58
+
59
+ No additional plugin configuration is needed. The plugin works out of the box once installed.
60
+
61
+ ## Stored value format
62
+
63
+ The field stores a JSON string in the database:
64
+
65
+ ```json
66
+ { "country": "AE", "region": "AZ" }
67
+ ```
68
+
69
+ ### API response
70
+
71
+ The plugin automatically parses stored JSON strings into objects in all `/api/*` responses via Koa middleware:
72
+
73
+ ```json
74
+ {
75
+ "data": {
76
+ "id": 1,
77
+ "name": "Burj Al Arab Jumeirah",
78
+ "location": {
79
+ "country": "AE",
80
+ "region": "AZ"
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ No extra configuration or population is required — it works automatically for all content types using the Region Select field.
87
+
88
+ ## Compatibility
89
+
90
+ | Requirement | Version |
91
+ |-------------|---------|
92
+ | Strapi | 5.x |
93
+ | Node.js | 20+ |
94
+
95
+ ## License
96
+
97
+ [MIT](LICENSE)
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const react = require("react");
5
+ const reactIntl = require("react-intl");
6
+ const designSystem = require("@strapi/design-system");
7
+ const reactTooltip = require("@radix-ui/react-tooltip");
8
+ const index = require("./index-C8USiGWk.js");
9
+ const PLUGIN_ID = "strapi-plugin-region";
10
+ const Input = react.forwardRef((props, ref) => {
11
+ const { name, value, onChange, label, error, hint, required, disabled, attribute } = props;
12
+ const { formatMessage } = reactIntl.useIntl();
13
+ const { countries: allCountries } = index.useCountries();
14
+ const mode = attribute?.options?.mode || "all";
15
+ const configuredCodes = react.useMemo(() => {
16
+ const raw = attribute?.options?.countries;
17
+ if (!Array.isArray(raw)) return [];
18
+ return raw.map((c) => c.trim().toUpperCase()).filter(Boolean);
19
+ }, [attribute?.options?.countries]);
20
+ const parsed = react.useMemo(() => {
21
+ if (!value) return null;
22
+ if (typeof value === "string") {
23
+ try {
24
+ return JSON.parse(value);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ return value;
30
+ }, [value]);
31
+ const selectedCountry = parsed?.country || null;
32
+ const selectedRegion = parsed?.region || null;
33
+ const countries = react.useMemo(() => {
34
+ if (mode === "all" || configuredCodes.length === 0) return allCountries;
35
+ if (mode === "only") {
36
+ return allCountries.filter((c) => configuredCodes.includes(c.countryShortCode));
37
+ }
38
+ if (mode === "except") {
39
+ return allCountries.filter((c) => !configuredCodes.includes(c.countryShortCode));
40
+ }
41
+ return allCountries;
42
+ }, [mode, configuredCodes, allCountries]);
43
+ const countryData = react.useMemo(
44
+ () => allCountries.find((c) => c.countryShortCode === selectedCountry) ?? null,
45
+ [selectedCountry, allCountries]
46
+ );
47
+ const regions = countryData?.regions ?? [];
48
+ react.useMemo(
49
+ () => regions.find((r) => r.shortCode === selectedRegion) ?? null,
50
+ [selectedRegion, regions]
51
+ );
52
+ const handleCountryChange = (countryCode) => {
53
+ onChange({
54
+ target: {
55
+ name,
56
+ value: countryCode ? JSON.stringify({ country: countryCode, region: null }) : null,
57
+ type: "string"
58
+ }
59
+ });
60
+ };
61
+ const handleRegionChange = (regionCode) => {
62
+ if (!selectedCountry || !regionCode) return;
63
+ onChange({
64
+ target: {
65
+ name,
66
+ value: JSON.stringify({ country: selectedCountry, region: regionCode }),
67
+ type: "string"
68
+ }
69
+ });
70
+ };
71
+ return /* @__PURE__ */ jsxRuntime.jsx(reactTooltip.Provider, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name, id: name, error, hint, required, ref, children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
73
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
74
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsx(
75
+ designSystem.Combobox,
76
+ {
77
+ value: selectedCountry,
78
+ onChange: handleCountryChange,
79
+ onClear: () => handleCountryChange(void 0),
80
+ clearLabel: formatMessage({
81
+ id: `${PLUGIN_ID}.clear.country`,
82
+ defaultMessage: "Clear country"
83
+ }),
84
+ disabled,
85
+ placeholder: formatMessage({
86
+ id: `${PLUGIN_ID}.placeholder.country`,
87
+ defaultMessage: "Select country..."
88
+ }),
89
+ autocomplete: { type: "both", filter: "startsWith" },
90
+ children: countries.map((c) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: c.countryShortCode, children: c.countryName }, c.countryShortCode))
91
+ }
92
+ ) }),
93
+ selectedCountry && regions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsx(
94
+ designSystem.Combobox,
95
+ {
96
+ value: selectedRegion,
97
+ onChange: handleRegionChange,
98
+ onClear: () => {
99
+ onChange({
100
+ target: {
101
+ name,
102
+ value: JSON.stringify({ country: selectedCountry, region: null }),
103
+ type: "string"
104
+ }
105
+ });
106
+ },
107
+ clearLabel: formatMessage({
108
+ id: `${PLUGIN_ID}.clear.region`,
109
+ defaultMessage: "Clear region"
110
+ }),
111
+ disabled,
112
+ placeholder: formatMessage({
113
+ id: `${PLUGIN_ID}.placeholder.region`,
114
+ defaultMessage: "Select region..."
115
+ }),
116
+ autocomplete: { type: "both", filter: "startsWith" },
117
+ children: regions.map((r) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: r.shortCode, children: r.name }, r.shortCode))
118
+ },
119
+ selectedCountry
120
+ ) })
121
+ ] }),
122
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
123
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
124
+ ] }) });
125
+ });
126
+ Input.displayName = "RegionSelectInput";
127
+ exports.default = Input;
@@ -0,0 +1,127 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useMemo } from "react";
3
+ import { useIntl } from "react-intl";
4
+ import { Field, Grid, Combobox, ComboboxOption } from "@strapi/design-system";
5
+ import { Provider } from "@radix-ui/react-tooltip";
6
+ import { u as useCountries } from "./index-Cpq91_9N.mjs";
7
+ const PLUGIN_ID = "strapi-plugin-region";
8
+ const Input = forwardRef((props, ref) => {
9
+ const { name, value, onChange, label, error, hint, required, disabled, attribute } = props;
10
+ const { formatMessage } = useIntl();
11
+ const { countries: allCountries } = useCountries();
12
+ const mode = attribute?.options?.mode || "all";
13
+ const configuredCodes = useMemo(() => {
14
+ const raw = attribute?.options?.countries;
15
+ if (!Array.isArray(raw)) return [];
16
+ return raw.map((c) => c.trim().toUpperCase()).filter(Boolean);
17
+ }, [attribute?.options?.countries]);
18
+ const parsed = useMemo(() => {
19
+ if (!value) return null;
20
+ if (typeof value === "string") {
21
+ try {
22
+ return JSON.parse(value);
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ return value;
28
+ }, [value]);
29
+ const selectedCountry = parsed?.country || null;
30
+ const selectedRegion = parsed?.region || null;
31
+ const countries = useMemo(() => {
32
+ if (mode === "all" || configuredCodes.length === 0) return allCountries;
33
+ if (mode === "only") {
34
+ return allCountries.filter((c) => configuredCodes.includes(c.countryShortCode));
35
+ }
36
+ if (mode === "except") {
37
+ return allCountries.filter((c) => !configuredCodes.includes(c.countryShortCode));
38
+ }
39
+ return allCountries;
40
+ }, [mode, configuredCodes, allCountries]);
41
+ const countryData = useMemo(
42
+ () => allCountries.find((c) => c.countryShortCode === selectedCountry) ?? null,
43
+ [selectedCountry, allCountries]
44
+ );
45
+ const regions = countryData?.regions ?? [];
46
+ useMemo(
47
+ () => regions.find((r) => r.shortCode === selectedRegion) ?? null,
48
+ [selectedRegion, regions]
49
+ );
50
+ const handleCountryChange = (countryCode) => {
51
+ onChange({
52
+ target: {
53
+ name,
54
+ value: countryCode ? JSON.stringify({ country: countryCode, region: null }) : null,
55
+ type: "string"
56
+ }
57
+ });
58
+ };
59
+ const handleRegionChange = (regionCode) => {
60
+ if (!selectedCountry || !regionCode) return;
61
+ onChange({
62
+ target: {
63
+ name,
64
+ value: JSON.stringify({ country: selectedCountry, region: regionCode }),
65
+ type: "string"
66
+ }
67
+ });
68
+ };
69
+ return /* @__PURE__ */ jsx(Provider, { children: /* @__PURE__ */ jsxs(Field.Root, { name, id: name, error, hint, required, ref, children: [
70
+ /* @__PURE__ */ jsx(Field.Label, { children: label }),
71
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
72
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsx(
73
+ Combobox,
74
+ {
75
+ value: selectedCountry,
76
+ onChange: handleCountryChange,
77
+ onClear: () => handleCountryChange(void 0),
78
+ clearLabel: formatMessage({
79
+ id: `${PLUGIN_ID}.clear.country`,
80
+ defaultMessage: "Clear country"
81
+ }),
82
+ disabled,
83
+ placeholder: formatMessage({
84
+ id: `${PLUGIN_ID}.placeholder.country`,
85
+ defaultMessage: "Select country..."
86
+ }),
87
+ autocomplete: { type: "both", filter: "startsWith" },
88
+ children: countries.map((c) => /* @__PURE__ */ jsx(ComboboxOption, { value: c.countryShortCode, children: c.countryName }, c.countryShortCode))
89
+ }
90
+ ) }),
91
+ selectedCountry && regions.length > 0 && /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsx(
92
+ Combobox,
93
+ {
94
+ value: selectedRegion,
95
+ onChange: handleRegionChange,
96
+ onClear: () => {
97
+ onChange({
98
+ target: {
99
+ name,
100
+ value: JSON.stringify({ country: selectedCountry, region: null }),
101
+ type: "string"
102
+ }
103
+ });
104
+ },
105
+ clearLabel: formatMessage({
106
+ id: `${PLUGIN_ID}.clear.region`,
107
+ defaultMessage: "Clear region"
108
+ }),
109
+ disabled,
110
+ placeholder: formatMessage({
111
+ id: `${PLUGIN_ID}.placeholder.region`,
112
+ defaultMessage: "Select region..."
113
+ }),
114
+ autocomplete: { type: "both", filter: "startsWith" },
115
+ children: regions.map((r) => /* @__PURE__ */ jsx(ComboboxOption, { value: r.shortCode, children: r.name }, r.shortCode))
116
+ },
117
+ selectedCountry
118
+ ) })
119
+ ] }),
120
+ /* @__PURE__ */ jsx(Field.Hint, {}),
121
+ /* @__PURE__ */ jsx(Field.Error, {})
122
+ ] }) });
123
+ });
124
+ Input.displayName = "RegionSelectInput";
125
+ export {
126
+ Input as default
127
+ };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const label = "Region Select";
4
+ const description = "Select country and region";
5
+ const en = {
6
+ label,
7
+ description,
8
+ "placeholder.country": "Select country...",
9
+ "placeholder.region": "Select region...",
10
+ "options.section.countries": "Country filtering",
11
+ "options.mode.label": "Mode",
12
+ "options.mode.description": "How to filter the list of available countries",
13
+ "options.mode.all": "All countries",
14
+ "options.mode.only": "Only selected",
15
+ "options.mode.except": "All except selected",
16
+ "options.countries.label": "Countries",
17
+ "options.countries.description": "One country code per line (ISO alpha-2, e.g. MV, AE, TR)",
18
+ "options.countries.placeholder": "MV\nAE\nTR",
19
+ "clear.country": "Clear country",
20
+ "clear.region": "Clear region",
21
+ "options.advanced.requiredField": "Required field",
22
+ "options.advanced.requiredField.description": "You won't be able to create an entry if this field is empty"
23
+ };
24
+ exports.default = en;
25
+ exports.description = description;
26
+ exports.label = label;
@@ -0,0 +1,26 @@
1
+ const label = "Region Select";
2
+ const description = "Select country and region";
3
+ const en = {
4
+ label,
5
+ description,
6
+ "placeholder.country": "Select country...",
7
+ "placeholder.region": "Select region...",
8
+ "options.section.countries": "Country filtering",
9
+ "options.mode.label": "Mode",
10
+ "options.mode.description": "How to filter the list of available countries",
11
+ "options.mode.all": "All countries",
12
+ "options.mode.only": "Only selected",
13
+ "options.mode.except": "All except selected",
14
+ "options.countries.label": "Countries",
15
+ "options.countries.description": "One country code per line (ISO alpha-2, e.g. MV, AE, TR)",
16
+ "options.countries.placeholder": "MV\nAE\nTR",
17
+ "clear.country": "Clear country",
18
+ "clear.region": "Clear region",
19
+ "options.advanced.requiredField": "Required field",
20
+ "options.advanced.requiredField.description": "You won't be able to create an entry if this field is empty"
21
+ };
22
+ export {
23
+ en as default,
24
+ description,
25
+ label
26
+ };