Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions app/helpers/i18n_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module I18nHelper
# Builds i18n translations hash with conditional loading based on controller/action
#
# This method ensures that only the necessary i18n translations are loaded
# for each page, improving performance by reducing the JavaScript payload.
#
# @return [Hash] Translations hash with core translations and conditional page-specific translations
#
# @example
# # In a view template:
# <div id="i18n-translations" data-translations="<%= i18n_translations_for_javascript.to_json %>"></div>
def i18n_translations_for_javascript
translations = {
I18n.locale.to_sym => {
core: I18n::JS.translations[I18n.locale.to_sym][:core]
}
}

# Add conditional translations based on controller/action
add_conditional_translations(translations)

translations
end

private

# Adds conditional translations based on the current page context
#
# @param translations [Hash] The base translations hash to modify
def add_conditional_translations(translations)
# Home feed page - include no-results translations
return unless home_feed_page?

translations[I18n.locale.to_sym][:views] = {
stories: {
feed: {
no_results: I18n.t("views.stories.feed.no_results")
}
}
}

# Add more conditional translations here as needed
# Example:
# if articles_show_page?
# translations[I18n.locale.to_sym][:views] ||= {}
# translations[I18n.locale.to_sym][:views][:articles] = {
# actions: I18n.t('views.articles.actions')
# }
# end
end

# Determines if the current page is the home feed page
#
# @return [Boolean] true if on stories#index, false otherwise
def home_feed_page?
controller_name == "stories" && action_name == "index" && @home_page == true
end

# Add more page detection methods as needed
# def articles_show_page?
# controller_name == 'articles' && action_name == 'show'
# end
end
33 changes: 19 additions & 14 deletions app/javascript/articles/Feed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => {
const [imageItem, setimageItem] = useState(null);
const [feedItems, setFeedItems] = useState([]);
const [onError, setOnError] = useState(false);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
async function fetchFeedItems(timeFrame = '', page = 1) {
Expand Down Expand Up @@ -74,6 +75,7 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => {
const organizeFeedItems = async () => {
try {
if (onError) setOnError(false);
setIsLoading(true);

fetchFeedItems(timeFrame).then(
([
Expand All @@ -82,19 +84,8 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => {
feedSecondBillboard,
feedThirdBillboard,
]) => {
if (feedPosts.length === 0) {
feedPosts.push({
id: 'dummy-story',
title: '👻 Nothing to see here',
description: 'Check back later for updates.',
type_of: 'status',
body_preview: '<strong>Follow some members and tags to make the most of your feed</strong>',
main_image: null,
pinned: false,
url: '/welcome',
reading_time: 0
});
} else {
// Set feed config if available
if (feedPosts.length > 0) {
const firstPost = feedPosts[0];
if (firstPost.feed_config) {
document.getElementById('index-container').dataset.feedConfigId = firstPost.feed_config;
Expand Down Expand Up @@ -128,11 +119,24 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => {
feedThirdBillboard,
);

setFeedItems(organizedFeedItemsWithBillboards);
// Only set feed items if we have actual content (not just billboards)
// This allows the parent component to handle empty states properly
const hasActualContent = organizedFeedItemsWithBillboards.some(item =>
typeof item === 'object' && item.id && item.id !== 'dummy-story'
);

if (hasActualContent || organizedFeedItemsWithBillboards.length === 0) {
setFeedItems(organizedFeedItemsWithBillboards);
} else {
// If we only have billboards, still set them but mark as empty content
setFeedItems([]);
}
setIsLoading(false);
},
);
} catch {
if (!onError) setOnError(true);
setIsLoading(false);
}
};
organizeFeedItems();
Expand Down Expand Up @@ -324,6 +328,7 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => {
feedItems,
bookmarkedFeedItems,
bookmarkClick,
isLoading,
})
)}
</div>
Expand Down
23 changes: 21 additions & 2 deletions app/javascript/packs/homePageFeed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TodaysPodcasts, PodcastEpisode } from '../podcasts';
import { articlePropTypes } from '../common-prop-types';
import { createRootFragment } from '../shared/preact/preact-root-fragment';
import { getUserDataAndCsrfToken } from '@utilities/getUserDataAndCsrfToken';
import { NoResults } from '../shared/components/NoResults';

/**
* Sends analytics about the featured article.
Expand Down Expand Up @@ -145,12 +146,30 @@ export const renderFeed = async (timeFrame, afterRender) => {
feedItems,
bookmarkedFeedItems,
bookmarkClick,
isLoading,
}) => {
if (feedItems.length === 0) {
// Fancy loading ✨
// Show loading state while fetching data
if (isLoading) {
return <FeedLoading />;
}

// Check if we have actual content (not just billboards)
const hasActualContent = feedItems.some(item =>
typeof item === 'object' && item.id && item.id !== 'dummy-story'
);

if (feedItems.length === 0 || !hasActualContent) {
// Determine feed type from localStorage or URL
const feedTypeOf = localStorage?.getItem('current_feed') || 'discover';
const feedType = feedTypeOf === 'following' ? 'following' : 'discover';

return (
<NoResults
feedType={feedType}
/>
);
}

return (
<Fragment>
{feedConstruct(
Expand Down
56 changes: 56 additions & 0 deletions app/javascript/shared/components/NoResults.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import { locale } from '../../utilities/locale';

/**
* A reusable component for displaying no results/empty states
* Follows the existing design patterns used throughout the app
*/
export const NoResults = ({
feedType = 'default',
title = null,
description = null,
actionText = null,
actionHref = null,
className = ""
}) => {
// Get i18n keys based on feed type
const getI18nKey = (key) => `views.stories.feed.no_results.${feedType}.${key}`;

// Use provided props or fall back to i18n
const displayTitle = title || locale(getI18nKey('title'));
const displayDescription = description || locale(getI18nKey('description'));
const displayActionText = actionText || locale(getI18nKey('action_text'));
const displayActionHref = actionHref || locale(getI18nKey('action_href'));

return (
<div className={`p-6 m:p-9 crayons-card crayons-card--secondary align-center fs-l h-100 flex items-center justify-center flex-1 mt-4 ${className}`}>
<div className="text-center">
<h2 className="crayons-subtitle-2 mb-2 color-base-80">
{displayTitle}
</h2>
<p className="color-base-60 mb-6">
{displayDescription}
</p>
{displayActionText && displayActionHref && (
<p>
<a href={displayActionHref} className="crayons-btn crayons-btn--l" data-no-instant>
{displayActionText}
</a>
</p>
)}
</div>
</div>
);
};

NoResults.propTypes = {
feedType: PropTypes.oneOf(['discover', 'following', 'default']),
title: PropTypes.string,
description: PropTypes.string,
actionText: PropTypes.string,
actionHref: PropTypes.string,
className: PropTypes.string,
};

NoResults.displayName = 'NoResults';
16 changes: 15 additions & 1 deletion app/models/subforem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Subforem < ApplicationRecord
before_validation :downcase_domain
after_save :bust_caches

def self.create_from_scratch!(domain:, brain_dump:, name:, logo_url:, bg_image_url: nil, default_locale: 'en')
def self.create_from_scratch!(domain:, brain_dump:, name:, logo_url:, bg_image_url: nil, default_locale: "en")
subforem = Subforem.create!(domain: domain)

# Queue background job for AI services
Expand Down Expand Up @@ -102,6 +102,18 @@ def self.cached_postable_array
end
end

def self.cached_misc_subforem_id
Rails.cache.fetch("subforem_misc_id", expires_in: 12.hours) do
Subforem.find_by(misc: true)&.id
end
end

def self.misc_subforem
Rails.cache.fetch("subforem_misc_object", expires_in: 12.hours) do
Subforem.find_by(misc: true)
end
end

def data_info_to_json
DataInfo.to_json(object: self, class_name: "Subforem", id: id, style: "full")
end
Expand Down Expand Up @@ -136,6 +148,8 @@ def bust_caches
Rails.cache.delete("subforem_all_domains")
Rails.cache.delete("subforem_default_id")
Rails.cache.delete("subforem_id_by_domain_#{domain}")
Rails.cache.delete("subforem_misc_id")
Rails.cache.delete("subforem_misc_object")
end

def downcase_domain
Expand Down
Loading
Loading