feat: Add search functionality for public portal (#5683)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
bce0bb8acb
commit
1fb1be3ddc
16 changed files with 378 additions and 13 deletions
|
@ -1,7 +1,7 @@
|
|||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :set_category
|
||||
before_action :set_category, except: [:index]
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
|
@ -20,7 +20,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
|||
end
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(slug: params[:category_slug])
|
||||
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
||||
end
|
||||
|
||||
def portal
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
// a relevant structure within app/javascript and only use these pack files to reference
|
||||
// that code so that it will be compiled.
|
||||
|
||||
import Vue from 'vue';
|
||||
import Rails from '@rails/ujs';
|
||||
import Turbolinks from 'turbolinks';
|
||||
import PublicArticleSearch from '../portal/components/PublicArticleSearch.vue';
|
||||
|
||||
import { navigateToLocalePage } from '../portal/portalHelpers';
|
||||
|
||||
|
@ -13,4 +15,21 @@ import '../portal/application.scss';
|
|||
Rails.start();
|
||||
Turbolinks.start();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', navigateToLocalePage);
|
||||
const initPageSetUp = () => {
|
||||
navigateToLocalePage();
|
||||
const isSearchContainerAvailable = document.querySelector('#search-wrap');
|
||||
if (isSearchContainerAvailable) {
|
||||
new Vue({
|
||||
components: { PublicArticleSearch },
|
||||
template: '<PublicArticleSearch />',
|
||||
}).$mount('#search-wrap');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initPageSetUp();
|
||||
});
|
||||
|
||||
document.addEventListener('turbolinks:load', () => {
|
||||
initPageSetUp();
|
||||
});
|
||||
|
|
14
app/javascript/portal/api/article.js
Normal file
14
app/javascript/portal/api/article.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import axios from 'axios';
|
||||
|
||||
class ArticlesAPI {
|
||||
constructor() {
|
||||
this.baseUrl = '';
|
||||
}
|
||||
|
||||
searchArticles(portalSlug, locale, query) {
|
||||
let baseUrl = `${this.baseUrl}/hc/${portalSlug}/${locale}/articles.json?query=${query}`;
|
||||
return axios.get(baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ArticlesAPI();
|
129
app/javascript/portal/components/PublicArticleSearch.vue
Normal file
129
app/javascript/portal/components/PublicArticleSearch.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div
|
||||
v-on-clickaway="closeSearch"
|
||||
class="mx-auto max-w-md w-full relative my-4"
|
||||
>
|
||||
<public-search-input
|
||||
v-model="searchTerm"
|
||||
:search-placeholder="searchTranslations.searchPlaceholder"
|
||||
@focus="openSearch"
|
||||
/>
|
||||
<div
|
||||
v-if="shouldShowSearchBox"
|
||||
class="absolute show-search-box w-full"
|
||||
@mouseover="openSearch"
|
||||
>
|
||||
<search-suggestions
|
||||
:items="searchResults"
|
||||
:is-loading="isLoading"
|
||||
:empty-placeholder="searchTranslations.emptyPlaceholder"
|
||||
:results-title="searchTranslations.resultsTitle"
|
||||
:loading-placeholder="searchTranslations.loadingPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import SearchSuggestions from './SearchSuggestions';
|
||||
import PublicSearchInput from './PublicSearchInput';
|
||||
|
||||
import ArticlesAPI from '../api/article';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PublicSearchInput,
|
||||
SearchSuggestions,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
isLoading: false,
|
||||
showSearchBox: false,
|
||||
searchResults: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
portalSlug() {
|
||||
return window.portalConfig.portalSlug;
|
||||
},
|
||||
localeCode() {
|
||||
return window.portalConfig.localeCode;
|
||||
},
|
||||
shouldShowSearchBox() {
|
||||
return this.searchTerm !== '' && this.showSearchBox;
|
||||
},
|
||||
searchTranslations() {
|
||||
const { searchTranslations = {} } = window.portalConfig;
|
||||
return searchTranslations;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
searchTerm() {
|
||||
if (this.typingTimer) {
|
||||
clearTimeout(this.typingTimer);
|
||||
}
|
||||
|
||||
this.openSearch();
|
||||
this.isLoading = true;
|
||||
this.typingTimer = setTimeout(() => {
|
||||
this.fetchArticlesByQuery();
|
||||
}, 1000);
|
||||
},
|
||||
currentPage() {
|
||||
this.clearSearchTerm();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChange(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
onBlur(e) {
|
||||
this.$emit('blur', e.target.value);
|
||||
},
|
||||
openSearch() {
|
||||
this.showSearchBox = true;
|
||||
},
|
||||
closeSearch() {
|
||||
this.showSearchBox = false;
|
||||
},
|
||||
clearSearchTerm() {
|
||||
this.searchTerm = '';
|
||||
},
|
||||
async fetchArticlesByQuery() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.searchResults = [];
|
||||
const { data } = await ArticlesAPI.searchArticles(
|
||||
this.portalSlug,
|
||||
this.localeCode,
|
||||
this.searchTerm
|
||||
);
|
||||
this.searchResults = data.payload;
|
||||
this.isLoading = true;
|
||||
} catch (error) {
|
||||
// Show something wrong message
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.show-search-box {
|
||||
top: 4rem;
|
||||
}
|
||||
</style>
|
60
app/javascript/portal/components/PublicSearchInput.vue
Normal file
60
app/javascript/portal/components/PublicSearchInput.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div
|
||||
class="w-full flex items-center rounded-md border-solid h-16 bg-white px-4 py-2 text-slate-600"
|
||||
:class="{
|
||||
'shadow border-2 border-woot-100': isFocused,
|
||||
'border border-slate-50 shadow-sm': !isFocused,
|
||||
}"
|
||||
>
|
||||
<fluent-icon icon="search" />
|
||||
<input
|
||||
:value="value"
|
||||
type="text"
|
||||
class="w-full search-input focus:outline-none text-base h-full bg-white px-2 py-2
|
||||
text-slate-700 placeholder-slate-500 sm:text-sm"
|
||||
:placeholder="searchPlaceholder"
|
||||
role="search"
|
||||
@input="onChange"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFocused: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
onFocus(e) {
|
||||
this.isFocused = true;
|
||||
this.$emit('focus', e.target.value);
|
||||
},
|
||||
onBlur(e) {
|
||||
this.isFocused = false;
|
||||
this.$emit('blur', e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
103
app/javascript/portal/components/SearchSuggestions.vue
Normal file
103
app/javascript/portal/components/SearchSuggestions.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div
|
||||
class="shadow-md bg-white mt-2 max-h-72 scroll-py-2 p-4 rounded overflow-y-auto text-sm text-slate-700"
|
||||
>
|
||||
<div v-if="isLoading" class="font-medium text-sm text-slate-400">
|
||||
{{ loadingPlaceholder }}
|
||||
</div>
|
||||
<h3 v-if="shouldShowResults" class="font-medium text-sm text-slate-400">
|
||||
{{ resultsTitle }}
|
||||
</h3>
|
||||
<ul
|
||||
v-if="shouldShowResults"
|
||||
class="bg-white mt-2 max-h-72 scroll-py-2 overflow-y-auto text-sm text-slate-700"
|
||||
role="listbox"
|
||||
>
|
||||
<li
|
||||
v-for="(article, index) in items"
|
||||
:id="article.id"
|
||||
:key="article.id"
|
||||
class="group flex cursor-default select-none items-center rounded-md p-2 mb-1"
|
||||
:class="{ 'bg-slate-25': index === selectedIndex }"
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
@mouseover="onHover(index)"
|
||||
>
|
||||
<a
|
||||
:href="generateArticleUrl(article)"
|
||||
class="flex-auto truncate text-base font-medium leading-6 w-full hover:underline"
|
||||
>
|
||||
{{ article.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="showEmptyResults" class="font-medium text-sm text-slate-400">
|
||||
{{ emptyPlaceholder }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [mentionSelectionKeyboardMixin],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
emptyPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loadingPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
resultsTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
showEmptyResults() {
|
||||
return !this.items.length && !this.isLoading;
|
||||
},
|
||||
shouldShowResults() {
|
||||
return this.items.length && !this.isLoading;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
generateArticleUrl(article) {
|
||||
return `/hc/${article.portal.slug}/${article.category.locale}/${article.category.slug}/${article.id}`;
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
this.$el.scrollTop = 40 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onSelect() {
|
||||
window.location = this.generateArticleUrl(this.items[this.selectedIndex]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,8 +1,13 @@
|
|||
export const navigateToLocalePage = () => {
|
||||
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||
|
||||
if (!allLocaleSwitcher) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { portalSlug } = allLocaleSwitcher.dataset;
|
||||
allLocaleSwitcher.addEventListener('change', event => {
|
||||
window.location = `/hc/${portalSlug}/${event.target.value}/`;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
"search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z",
|
||||
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
|
||||
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z","M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z","M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
|
||||
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z", "M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z", "M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ By default, it renders:
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<meta name= "turbolinks-cache-control" content= "no-cache">
|
||||
<%= javascript_pack_tag 'portal' %>
|
||||
<%= stylesheet_pack_tag 'portal' %>
|
||||
<%= csrf_meta_tags %>
|
||||
|
@ -35,4 +36,16 @@ By default, it renders:
|
|||
</main>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
window.portalConfig = {
|
||||
portalSlug: '<%= @portal.slug %>',
|
||||
localeCode: '<%= @locale %>',
|
||||
searchTranslations: {
|
||||
searchPlaceholder: '<%= I18n.t('public_portal.search.search_placeholder') %>',
|
||||
emptyPlaceholder: '<%= I18n.t('public_portal.search.empty_placeholder') %>',
|
||||
loadingPlaceholder: '<%= I18n.t('public_portal.search.loading_placeholder') %>',
|
||||
resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -13,6 +13,14 @@ if article.portal.present?
|
|||
end
|
||||
end
|
||||
|
||||
if article.category.present?
|
||||
json.category do
|
||||
json.id article.category.id
|
||||
json.slug article.category.slug
|
||||
json.locale article.category.locale
|
||||
end
|
||||
end
|
||||
|
||||
json.views article.views
|
||||
|
||||
if article.author.present?
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<footer class="pt-16 pb-8 flex flex-col items-center justify-center">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<p class="text-slate-700 py-2 text-center">
|
||||
Made with <a class="hover:underline" href="https://www.chatwoot.com" target="_blank" rel="noopener noreferrer nofoll/ow">Chatwoot</a> 💙.
|
||||
Made with <a class="hover:underline" href="https://www.chatwoot.com" target="_blank" rel="noopener noreferrer nofoll/ow">Chatwoot</a> 💙
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="inline-flex relative w-24">
|
||||
<select
|
||||
data-portal-slug="<%= @portal.slug %>"
|
||||
class="appearance-none w-full bg-white px-3 py-2 pr-8 rounded leading-tight focus:outline-none focus:shadow-outline locale-switcher"
|
||||
class="appearance-none w-full bg-white px-3 py-2 pr-8 rounded leading-tight focus:outline-none focus:shadow-outline locale-switcher hover:bg-slate-75 cursor-pointer"
|
||||
>
|
||||
<% @portal.config["allowed_locales"].each do |locale| %>
|
||||
<option <%= locale == params[:locale] ? 'selected': '' %> value="<%= locale %>"><%= locale %></option>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
|
||||
<section class="bg-gradient-to-b from-white to-black-50 pt-8 pb-16 md:py-16 flex flex-col items-center justify-center">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-2xl px-5 md:text-4xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed text-center">
|
||||
<section class="bg-gradient-to-b from-white to-slate-75 pt-8 pb-16 md:py-16 flex flex-col items-center justify-center">
|
||||
<div class="mx-auto w-full max-w-2xl flex flex-col items-center">
|
||||
<h1 class="text-2xl px-5 md:text-4xl text-slate-900 font-semibold subpixel-antialiased leading-normal text-center">
|
||||
<%= portal.header_text %>
|
||||
</h1>
|
||||
<p class="text-slate-700 py-2 text-center mt-10">Browse the categories below</p>
|
||||
<p class="text-slate-600 py-2 text-center my-2 max-w-sm leading-normal"><%= I18n.t('public_portal.hero.sub_title') %></p>
|
||||
<div id="search-wrap"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
<div class="max-w-4xl px-6 py-16 mx-auto space-y-12 w-full">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<a class="text-slate-800 hover:underline leading-8"
|
||||
href="/hc/<%= @portal.slug %>/<%= @category.slug %>" class=""><%= @portal.name %> Home</a>
|
||||
<a
|
||||
class="text-slate-800 hover:underline leading-8"
|
||||
href="/hc/<%= @portal.slug %>/<%= @category.present? ? @category.slug : '' %>"
|
||||
>
|
||||
<%= @portal.name %> Home
|
||||
</a>
|
||||
<span>/</span>
|
||||
<span>/</span>
|
||||
</div>
|
||||
|
@ -23,7 +27,6 @@
|
|||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="max-w-4xl flex-grow w-full px-6 py-16 mx-auto space-y-12">
|
||||
<article class="space-y-8 ">
|
||||
|
|
|
@ -165,3 +165,11 @@ en:
|
|||
fullcontact:
|
||||
name: "Fullcontact"
|
||||
description: "FullContact integration helps to enrich visitor profiles. Identify the users as soon as they share their email address and offer them tailored customer service. Connect your FullContact to your account by sharing the FullContact API Key."
|
||||
public_portal:
|
||||
search:
|
||||
search_placeholder: Search for article by title or body...
|
||||
empty_placeholder: No results found.
|
||||
loading_placeholder: Searching...
|
||||
results_title: Search results
|
||||
hero:
|
||||
sub_title: Search for the articles here or browse the categories below.
|
||||
|
|
|
@ -290,6 +290,7 @@ Rails.application.routes.draw do
|
|||
|
||||
get 'hc/:slug', to: 'public/api/v1/portals#show'
|
||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
||||
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
|
||||
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
|
||||
get 'hc/:slug/:locale/:category_slug', to: 'public/api/v1/portals/categories#show'
|
||||
get 'hc/:slug/:locale/:category_slug/articles', to: 'public/api/v1/portals/articles#index'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue