Repository: jenkins-infra/stories
Pull request #143: feat: add `js-search`
Author: login: saurabhraghuvanshii
Created: 2025-03-08T14:51:36Z | Merged: 2025-03-13T08:24:25Z

Fixes #139. 

Added JS Search. 


Linked issue #139: add Search with JS Search
### What feature do you want to see added?

To add search with JS Search for this Gatsby site as outlined in https://www.gatsbyjs.com/docs/adding-search-with-js-search. 

### Upstream changes

N/A
Linked issue comments: user: login: biru-codeastromer
created_at: 2025-03-04T19:19:40Z
body: Hi @krisstern I haven't work on this repo for some time now . Can I work on this issue ?
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-04T19:49:23Z
body: Hi @biru-codeastromer if your goal is to apply for GSoC, then I would say no, it would be better for other people to work on this issue so they may have a chance too, as you have had many chances to showcase your abilities recently.  
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-04T19:49:28Z
body: @krisstern can I work on this I work on gatsby before .
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-04T19:50:42Z
body: @biru-codeastromer If you over participate we could potentially disqualify you. According to official Google guidelines you should not have opened more than 10 to 20 pull requests prior to applying for GSoC, unless you are applying for a second time. 
author_association: MEMBER
user: login: biru-codeastromer
created_at: 2025-03-04T19:59:56Z
body: > Hi [@biru-codeastromer](https://github.com/biru-codeastromer) if your goal is to apply for GSoC, then I would say no, it would be better for other people to work on this issue so they may have a chance too, as you have had many chances to showcase your abilities recently.

Okay  got it 😇
author_association: CONTRIBUTOR
user: login: biru-codeastromer
created_at: 2025-03-04T20:02:13Z
body: > [@biru-codeastromer](https://github.com/biru-codeastromer) If you over participate we could potentially disqualify you. According to official Google guidelines you should not have opened more than 10 to 20 pull requests prior to applying for GSoC, unless you are applying for a second time.



@krisstern  I didn't know about this rule . Sorry from my side . I would keep this in mind now . I got feared out 😥  and  certainly don’t want to over-participate or violate any guidelines. Hope you understand. and sorry again 🤗

Thanks very much for clarifying me this rule..It will help me a lot .
author_association: CONTRIBUTOR
user: login: saurabhraghuvanshii
created_at: 2025-03-04T20:12:08Z
body: @krisstern  stories have small data set or big?
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-04T20:16:22Z
body: @saurabhraghuvanshii The same of what I said to Birajit applied to you too
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-04T20:27:31Z
body: @krisstern sorry sir , I think I have less contribution therefore I try to do  one or two . extremely sorry.
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-04T21:02:19Z
body: Hi @saurabhraghuvanshii the total number of pull requests counted is for any open source contribution, especially the ones you made to the Jenkins community, not just this repo. 
author_association: MEMBER
user: login: SohamJuneja
created_at: 2025-03-04T21:28:29Z
body: I saw this issue and found it quite interesting. I understand your previous comments for GSoC applicants, so I wanted to confirm before taking any steps. 
author_association: CONTRIBUTOR
user: login: joshua-jinu
created_at: 2025-03-05T03:45:40Z
body: Hey @krisstern , I would love to work on this issue
author_association: CONTRIBUTOR
user: login: TenzDelek
created_at: 2025-03-05T04:01:00Z
body: I would love to work on this issue. to confirm, the search need to be on the user story right?  i realize that my previous pr #123  was definitely looks incomplete. I looked into the docs this afternoon, and did a little test in local. to my surprise the js search library really looks great.  i did the addindex on the title and a little twist with only triggering the search after the user atleast type 3 character. I also looked in to the demo https://bvaughn.github.io/js-search/ where i notice that we can sanitize the search with case sensitive, and have strategies like exact match, prefix matching and all. over all this look very interesting and i would like to take on this if possible
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-08T06:16:16Z
body: @saurabhraghuvanshii I have checked your GitHub history and think it should be okay for you to take this issuse. Please go ahead and follow up with a PR. 
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-08T06:18:11Z
body: Ok sir thanks 
author_association: CONTRIBUTOR

Diff:
diff --git a/package-lock.json b/package-lock.json
index 73f1f666..d183ba48 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
       "name": "stories-jenkins.io",
       "version": "1.0.0",
       "dependencies": {
+        "axios": "^1.8.2",
         "cheerio": "^1.0.0-rc.10",
         "eslint": "^8.12.0",
         "eslint-config-react-app": "^7.0.0",
@@ -25,6 +26,7 @@
         "gatsby-source-filesystem": "^4.10.1",
         "gatsby-transformer-remark": "^5.11.1",
         "gatsby-transformer-sharp": "^4.10.0",
+        "js-search": "^2.0.1",
         "leaflet": "^1.7.1",
         "netlify-cms-app": "^2.15.72",
         "react": "^17.0.1",
@@ -3053,6 +3055,17 @@
         "react-dom": "*"
       }
     },
+    "node_modules/@jenkinsci/gatsby-plugin-jenkins-layout/node_modules/axios": {
+      "version": "0.27.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.0"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.8",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -6365,14 +6378,14 @@
       }
     },
     "node_modules/axios": {
-      "version": "0.27.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
-      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
-      "dev": true,
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
+      "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
       "license": "MIT",
       "dependencies": {
-        "follow-redirects": "^1.14.9",
-        "form-data": "^4.0.0"
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
       }
     },
     "node_modules/axobject-query": {
@@ -15386,6 +15399,12 @@
       "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
       "license": "BSD-3-Clause"
     },
+    "node_modules/js-search": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/js-search/-/js-search-2.0.1.tgz",
+      "integrity": "sha512-8k12LiC3fPt7gLRJTc1azE1BFvlxIw+BG3J9YzjuYf4wSE65uqYSYP4VhweApcTfV51Fzq/ogBulQew5937A9A==",
+      "license": "MIT"
+    },
     "node_modules/js-sha256": {
       "version": "0.9.0",
       "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
@@ -20106,6 +20125,12 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
diff --git a/package.json b/package.json
index 202d4914..a6d29cf2 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
     "gatsby-source-filesystem": "^4.10.1",
     "gatsby-transformer-remark": "^5.11.1",
     "gatsby-transformer-sharp": "^4.10.0",
+    "js-search": "^2.0.1",
     "leaflet": "^1.7.1",
     "netlify-cms-app": "^2.15.72",
     "react": "^17.0.1",
diff --git a/src/components/SearchContainer.css b/src/components/SearchContainer.css
new file mode 100644
index 00000000..833b2dd8
--- /dev/null
+++ b/src/components/SearchContainer.css
@@ -0,0 +1,91 @@
+.stories-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 1.5rem;
+    padding: 1rem;
+}
+  
+.story-card {
+    transition: transform 0.2s ease-in-out;
+    color: inherit;
+}
+  
+.story-card:hover {
+    transform: translateY(-4px);
+}
+  
+.story-card .card {
+    background-color: var(--bs-dark);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+  
+.story-card:hover .card {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+    border-color: rgba(255, 255, 255, 0.2);
+}
+
+.story-card .card-title {
+    color: var(--bs-light);
+}
+
+.story-card .card-text {
+    color: rgba(255, 255, 255, 0.8);
+}
+
+.story-card .text-muted {
+    color: rgba(255, 255, 255, 0.6) !important;
+}
+
+.industries {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+}
+
+.industries .badge {
+    background-color: rgba(255, 255, 255, 0.1);
+    color: rgba(255, 255, 255, 0.9);
+    border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.search-container input {
+    background-color: var(--bs-dark);
+    border-color: rgba(255, 255, 255, 0.2);
+    color: var(--bs-light);
+}
+
+.search-container input:focus {
+    background-color: var(--bs-dark);
+    border-color: rgba(255, 255, 255, 0.4);
+    color: var(--bs-light);
+    box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.1);
+}
+
+.search-container input::placeholder {
+    color: rgba(255, 255, 255, 0.5);
+}
+
+.search-container .input-group-text {
+    background-color: rgba(255, 255, 255, 0.1);
+    border-color: rgba(255, 255, 255, 0.2);
+    color: rgba(255, 255, 255, 0.7);
+}
+
+.results-count {
+    color: rgba(255, 255, 255, 0.7);
+}
+
+.no-results {
+    color: rgba(255, 255, 255, 0.7);
+}
+
+.error-state {
+    color: rgba(255, 255, 255, 0.7);
+    text-align: center;
+    padding: 2rem;
+}
+
+.error-state {
+    color: var(--bs-danger);
+}
diff --git a/src/components/SearchContainer.jsx b/src/components/SearchContainer.jsx
new file mode 100644
index 00000000..4299e5a3
--- /dev/null
+++ b/src/components/SearchContainer.jsx
@@ -0,0 +1,191 @@
+import React, { Component } from 'react';
+import { Link, graphql, useStaticQuery } from 'gatsby';
+import './SearchContainer.css';
+import * as JsSearch from 'js-search';
+
+class Search extends Component {
+  state = {
+    stories: [],
+    search: null,
+    searchResults: [], 
+    isLoading: true,
+    isError: false,
+    searchQuery: '',
+  };
+
+  componentDidMount() {
+    const { allUserStory } = this.props;
+    try {
+      const stories = allUserStory.nodes.map(story => ({
+        title: story.title,
+        tag_line: story.tag_line,
+        authored_by: story.authored_by,
+        slug: story.slug,
+        date: story.date,
+        metadata: story.metadata,
+        body_content: story.body_content,
+      }));
+      
+      this.initializeSearch(stories);
+    } catch (err) {
+      this.setState({ isError: true });
+      console.log(`Error processing story data\n${err}`);
+    }
+  }
+
+  initializeSearch = (stories) => {
+    const dataToSearch = new JsSearch.Search('title');
+
+    dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy();
+    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
+    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex('title');
+
+    dataToSearch.addIndex('title');
+    dataToSearch.addIndex('tag_line');
+    dataToSearch.addIndex('authored_by');
+    dataToSearch.addIndex(['metadata', 'title']);
+    dataToSearch.addIndex(['metadata', 'industries']);
+    dataToSearch.addIndex(['metadata', 'programming_languages']);
+    dataToSearch.addIndex(['body_content', 'paragraphs']);
+
+    dataToSearch.addDocuments(stories);
+    this.setState({ 
+      stories,
+      search: dataToSearch, 
+      isLoading: false 
+    });
+  };
+
+  searchData = e => {
+    const searchQuery = e.target.value;
+    if (searchQuery) {
+      const queryResult = this.state.search.search(searchQuery);
+      this.setState({ 
+        searchQuery,
+        searchResults: queryResult 
+      });
+    } else {
+      this.setState({ 
+        searchQuery: '',
+        searchResults: [] 
+      });
+    }
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+  };
+
+  render() {
+    const { searchResults, searchQuery, isLoading, isError } = this.state;
+    
+    if (isLoading) {
+      return <div className="text-center p-4">Loading search...</div>;
+    }
+
+    if (isError) {
+      return <div className="text-center p-4 text-danger">Error loading search</div>;
+    }
+
+    return (
+      <div className="container py-4 search-container">
+        <form onSubmit={this.handleSubmit} className="mb-4">
+          <div className="row justify-content-center">
+            <div className="col-md-8">
+              <div className="input-group">
+                <input
+                  id="Search"
+                  className="form-control form-control-lg"
+                  value={searchQuery}
+                  onChange={this.searchData}
+                  placeholder="Search user stories..."
+                />
+                <span className="input-group-text">
+                  <ion-icon name="search-outline"></ion-icon>
+                </span>
+              </div>
+            </div>
+          </div>
+        </form>
+
+        {searchQuery && (
+          <div>
+            <p className="text-bold mb-4">
+              Found {searchResults.length} {searchResults.length === 1 ? 'story' : 'stories'} 
+              &nbsp;for "{searchQuery}"
+            </p>
+
+            {searchResults.length > 0 ? (
+              <div className="stories-grid">
+                {searchResults.map((item, index) => (
+                  <Link 
+                    to={`/user-story/${item.slug}`} 
+                    key={`story_${index}`}
+                    className="story-card text-decoration-none"
+                  >
+                    <div className="card h-100">
+                      <div className="card-body">
+                        <h3 className="h5 card-title">{item.title}</h3>
+                        <p className="card-text text-muted">{item.tag_line}</p>
+                        <div className="story-meta mb-2">
+                          <small className="text-muted">
+                            By {item.authored_by} • {new Date(item.date).toLocaleDateString()}
+                          </small>
+                        </div>
+                        {item.metadata?.industries && (
+                          <div className="industries">
+                            {item.metadata.industries.map((industry, i) => (
+                              <span key={i} className="badge bg-light text-dark me-1">
+                                {industry}
+                              </span>
+                            ))}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </Link>
+                ))}
+              </div>
+            ) : (
+              <div className="text-center text-muted">
+                <p>No stories found matching your search</p>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
+}
+
+const SearchContainer = () => {
+  const data = useStaticQuery(graphql`
+    query {
+      allUserStory {
+        nodes {
+          title
+          tag_line
+          authored_by
+          slug
+          date
+          metadata {
+            title
+            industries
+            programming_languages
+            platforms
+          }
+          body_content {
+            title
+            paragraphs {
+              html
+            }
+          }
+        }
+      }
+    }
+  `);
+
+  return <Search allUserStory={data.allUserStory} />;
+};
+
+export default SearchContainer;
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 3d5dead6..2f66dd3a 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,6 +1,7 @@
 import * as React from 'react';
 import { useStaticQuery, graphql, Link } from 'gatsby';
 import { StaticImage } from 'gatsby-plugin-image';
+import Search from '../components/SearchContainer';
 import Layout from '../layout';
 import Seo from '../components/Seo';
 import './index.css';
@@ -41,6 +42,15 @@ const IndexPage = () => {
         </div>
       </div>
 
+      <div>
+        <h1 style={{ marginTop: `3em`, textAlign: `center` }}>
+        Search Jenkins Stories
+        </h1>
+        <div>
+        <Search />
+        </div>
+      </div>
+
       <div className="stories-section">
         <h2 className="section-title">Latest Jenkins User Stories</h2>
         <div className="story-cards">


Reviews:
user: login: krisstern
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-08T16:09:45Z
user: login: krisstern
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-12T15:39:28Z
user: login: saurabhraghuvanshii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T07:30:16Z
user: login: krisstern
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T08:19:47Z
user: login: krisstern
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T08:24:01Z

Review comments:
diff_hunk: @@ -0,0 +1,191 @@
+import React, { Component } from 'react';
+import { Link, graphql, useStaticQuery } from 'gatsby';
+import '../cms/SearchContainer.css';
+import * as JsSearch from 'js-search';
+
+class Search extends Component {
+  state = {
+    stories: [],
+    search: null,
+    searchResults: [], 
+    isLoading: true,
+    isError: false,
+    searchQuery: '',
+  };
+
+  componentDidMount() {
+    const { allUserStory } = this.props;
+    try {
+      const stories = allUserStory.nodes.map(story => ({
+        title: story.title,
+        tag_line: story.tag_line,
+        authored_by: story.authored_by,
+        slug: story.slug,
+        date: story.date,
+        metadata: story.metadata,
+        body_content: story.body_content,
+      }));
+      
+      this.initializeSearch(stories);
+    } catch (err) {
+      this.setState({ isError: true });
+      console.log(`Error processing story data\n${err}`);
+    }
+  }
+
+  initializeSearch = (stories) => {
+    const dataToSearch = new JsSearch.Search('title');
+
+    dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy();
+    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
+    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex('title');
+
+    dataToSearch.addIndex('title');
+    dataToSearch.addIndex('tag_line');
+    dataToSearch.addIndex('authored_by');
+    dataToSearch.addIndex(['metadata', 'title']);
+    dataToSearch.addIndex(['metadata', 'industries']);
+    dataToSearch.addIndex(['metadata', 'programming_languages']);
+    dataToSearch.addIndex(['body_content', 'paragraphs']);
+
+    dataToSearch.addDocuments(stories);
+    this.setState({ 
+      stories,
+      search: dataToSearch, 
+      isLoading: false 
+    });
+  };
+
+  searchData = e => {
+    const searchQuery = e.target.value;
+    if (searchQuery) {
+      const queryResult = this.state.search.search(searchQuery);
+      this.setState({ 
+        searchQuery,
+        searchResults: queryResult 
+      });
+    } else {
+      this.setState({ 
+        searchQuery: '',
+        searchResults: [] 
+      });
+    }
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+  };
+
+  render() {
+    const { searchResults, searchQuery, isLoading, isError } = this.state;
+    
+    if (isLoading) {
+      return <div className="text-center p-4">Loading search...</div>;
+    }
+
+    if (isError) {
+      return <div className="text-center p-4 text-danger">Error loading search</div>;
+    }
+
+    return (
+      <div className="container py-4 search-container">
+        <form onSubmit={this.handleSubmit} className="mb-4">
+          <div className="row justify-content-center">
+            <div className="col-md-8">
+              <div className="input-group">
+                <input
+                  id="Search"
+                  className="form-control form-control-lg"
+                  value={searchQuery}
+                  onChange={this.searchData}
+                  placeholder="Search user stories by title, tag line, author, or content..."
path: src/components/SearchContainer.jsx
user: login: krisstern
body: ```suggestion
                  placeholder="Search user stories..."
```
created_at: 2025-03-08T16:09:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,191 @@
+import React, { Component } from 'react';
+import { Link, graphql, useStaticQuery } from 'gatsby';
+import '../cms/SearchContainer.css';
+import * as JsSearch from 'js-search';
+
+class Search extends Component {
+  state = {
+    stories: [],
+    search: null,
+    searchResults: [], 
+    isLoading: true,
+    isError: false,
+    searchQuery: '',
+  };
+
+  componentDidMount() {
+    const { allUserStory } = this.props;
+    try {
+      const stories = allUserStory.nodes.map(story => ({
+        title: story.title,
+        tag_line: story.tag_line,
+        authored_by: story.authored_by,
+        slug: story.slug,
+        date: story.date,
+        metadata: story.metadata,
+        body_content: story.body_content,
+      }));
+      
+      this.initializeSearch(stories);
+    } catch (err) {
+      this.setState({ isError: true });
+      console.log(`Error processing story data\n${err}`);
+    }
+  }
+
+  initializeSearch = (stories) => {
+    const dataToSearch = new JsSearch.Search('title');
+
+    dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy();
+    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
+    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex('title');
+
+    dataToSearch.addIndex('title');
+    dataToSearch.addIndex('tag_line');
+    dataToSearch.addIndex('authored_by');
+    dataToSearch.addIndex(['metadata', 'title']);
+    dataToSearch.addIndex(['metadata', 'industries']);
+    dataToSearch.addIndex(['metadata', 'programming_languages']);
+    dataToSearch.addIndex(['body_content', 'paragraphs']);
+
+    dataToSearch.addDocuments(stories);
+    this.setState({ 
+      stories,
+      search: dataToSearch, 
+      isLoading: false 
+    });
+  };
+
+  searchData = e => {
+    const searchQuery = e.target.value;
+    if (searchQuery) {
+      const queryResult = this.state.search.search(searchQuery);
+      this.setState({ 
+        searchQuery,
+        searchResults: queryResult 
+      });
+    } else {
+      this.setState({ 
+        searchQuery: '',
+        searchResults: [] 
+      });
+    }
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+  };
+
+  render() {
+    const { searchResults, searchQuery, isLoading, isError } = this.state;
+    
+    if (isLoading) {
+      return <div className="text-center p-4">Loading search...</div>;
+    }
+
+    if (isError) {
+      return <div className="text-center p-4 text-danger">Error loading search</div>;
+    }
+
+    return (
+      <div className="container py-4 search-container">
+        <form onSubmit={this.handleSubmit} className="mb-4">
+          <div className="row justify-content-center">
+            <div className="col-md-8">
+              <div className="input-group">
+                <input
+                  id="Search"
+                  className="form-control form-control-lg"
+                  value={searchQuery}
+                  onChange={this.searchData}
+                  placeholder="Search user stories..."
+                />
+                <span className="input-group-text">
+                  <ion-icon name="search-outline"></ion-icon>
+                </span>
+              </div>
+            </div>
+          </div>
+        </form>
+
+        {searchQuery && (
+          <div>
+            <p className="text-bold mb-4">
+              Found {searchResults.length} {searchResults.length === 1 ? 'story' : 'stories'} 
+              &nbsp;for "{searchQuery}"
+            </p>
+
+            {searchResults.length > 0 ? (
+              <div className="stories-grid">
+                {searchResults.map((item, index) => (
+                  <Link 
+                    to={`/user-story/${item.slug}`} 
+                    key={`story_${index}`}
+                    className="story-card text-decoration-none"
+                  >
+                    <div className="card h-100">
+                      <div className="card-body">
+                        <h3 className="h5 card-title">{item.title}</h3>
+                        <p className="card-text text-muted">{item.tag_line}</p>
+                        <div className="story-meta mb-2">
+                          <small className="text-muted">
+                            By {item.authored_by} • {new Date(item.date).toLocaleDateString()}
+                          </small>
+                        </div>
+                        {item.metadata?.industries && (
+                          <div className="industries">
+                            {item.metadata.industries.map((industry, i) => (
+                              <span key={i} className="badge bg-light text-dark me-1">
+                                {industry}
+                              </span>
+                            ))}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </Link>
+                ))}
+              </div>
+            ) : (
+              <div className="text-center text-muted">
+                <p>No stories found matching your search.</p>
path: src/components/SearchContainer.jsx
user: login: krisstern
body: ```suggestion
                <p>No stories found matching your search</p>
```
created_at: 2025-03-12T15:39:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,191 @@
+import React, { Component } from 'react';
+import { Link, graphql, useStaticQuery } from 'gatsby';
+import '../cms/SearchContainer.css';
+import * as JsSearch from 'js-search';
+
+class Search extends Component {
+  state = {
+    stories: [],
+    search: null,
+    searchResults: [], 
+    isLoading: true,
+    isError: false,
+    searchQuery: '',
+  };
+
+  componentDidMount() {
+    const { allUserStory } = this.props;
+    try {
+      const stories = allUserStory.nodes.map(story => ({
+        title: story.title,
+        tag_line: story.tag_line,
+        authored_by: story.authored_by,
+        slug: story.slug,
+        date: story.date,
+        metadata: story.metadata,
+        body_content: story.body_content,
+      }));
+      
+      this.initializeSearch(stories);
+    } catch (err) {
+      this.setState({ isError: true });
+      console.log(`Error processing story data\n${err}`);
+    }
+  }
+
+  initializeSearch = (stories) => {
+    const dataToSearch = new JsSearch.Search('title');
+
+    dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy();
+    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
+    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex('title');
+
+    dataToSearch.addIndex('title');
+    dataToSearch.addIndex('tag_line');
+    dataToSearch.addIndex('authored_by');
+    dataToSearch.addIndex(['metadata', 'title']);
+    dataToSearch.addIndex(['metadata', 'industries']);
+    dataToSearch.addIndex(['metadata', 'programming_languages']);
+    dataToSearch.addIndex(['body_content', 'paragraphs']);
+
+    dataToSearch.addDocuments(stories);
+    this.setState({ 
+      stories,
+      search: dataToSearch, 
+      isLoading: false 
+    });
+  };
+
+  searchData = e => {
+    const searchQuery = e.target.value;
+    if (searchQuery) {
+      const queryResult = this.state.search.search(searchQuery);
+      this.setState({ 
+        searchQuery,
+        searchResults: queryResult 
+      });
+    } else {
+      this.setState({ 
+        searchQuery: '',
+        searchResults: [] 
+      });
+    }
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+  };
+
+  render() {
+    const { searchResults, searchQuery, isLoading, isError } = this.state;
+    
+    if (isLoading) {
+      return <div className="text-center p-4">Loading search...</div>;
+    }
+
+    if (isError) {
+      return <div className="text-center p-4 text-danger">Error loading search</div>;
+    }
+
+    return (
+      <div className="container py-4 search-container">
+        <form onSubmit={this.handleSubmit} className="mb-4">
+          <div className="row justify-content-center">
+            <div className="col-md-8">
+              <div className="input-group">
+                <input
+                  id="Search"
+                  className="form-control form-control-lg"
+                  value={searchQuery}
+                  onChange={this.searchData}
+                  placeholder="Search user stories..."
+                />
+                <span className="input-group-text">
+                  <ion-icon name="search-outline"></ion-icon>
+                </span>
+              </div>
+            </div>
+          </div>
+        </form>
+
+        {searchQuery && (
+          <div>
+            <p className="text-bold mb-4">
+              Found {searchResults.length} {searchResults.length === 1 ? 'story' : 'stories'} 
+              &nbsp;for "{searchQuery}"
+            </p>
+
+            {searchResults.length > 0 ? (
+              <div className="stories-grid">
+                {searchResults.map((item, index) => (
+                  <Link 
+                    to={`/user-story/${item.slug}`} 
+                    key={`story_${index}`}
+                    className="story-card text-decoration-none"
+                  >
+                    <div className="card h-100">
+                      <div className="card-body">
+                        <h3 className="h5 card-title">{item.title}</h3>
+                        <p className="card-text text-muted">{item.tag_line}</p>
+                        <div className="story-meta mb-2">
+                          <small className="text-muted">
+                            By {item.authored_by} • {new Date(item.date).toLocaleDateString()}
+                          </small>
+                        </div>
+                        {item.metadata?.industries && (
+                          <div className="industries">
+                            {item.metadata.industries.map((industry, i) => (
+                              <span key={i} className="badge bg-light text-dark me-1">
+                                {industry}
+                              </span>
+                            ))}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </Link>
+                ))}
+              </div>
+            ) : (
+              <div className="text-center text-muted">
+                <p>No stories found matching your search.</p>
path: src/components/SearchContainer.jsx
user: login: saurabhraghuvanshii
body: @krisstern sorry sir for delay , my wsl2 give me network issue like firewall and it does not connect with vs code , I tried to solve it from yesterday , finally I  solved it with bunch of scripts and command , sorry one's again for delay .
created_at: 2025-03-13T07:30:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,191 @@
+import React, { Component } from 'react';
+import { Link, graphql, useStaticQuery } from 'gatsby';
+import '../cms/SearchContainer.css';
+import * as JsSearch from 'js-search';
+
+class Search extends Component {
+  state = {
+    stories: [],
+    search: null,
+    searchResults: [], 
+    isLoading: true,
+    isError: false,
+    searchQuery: '',
+  };
+
+  componentDidMount() {
+    const { allUserStory } = this.props;
+    try {
+      const stories = allUserStory.nodes.map(story => ({
+        title: story.title,
+        tag_line: story.tag_line,
+        authored_by: story.authored_by,
+        slug: story.slug,
+        date: story.date,
+        metadata: story.metadata,
+        body_content: story.body_content,
+      }));
+      
+      this.initializeSearch(stories);
+    } catch (err) {
+      this.setState({ isError: true });
+      console.log(`Error processing story data\n${err}`);
+    }
+  }
+
+  initializeSearch = (stories) => {
+    const dataToSearch = new JsSearch.Search('title');
+
+    dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy();
+    dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer();
+    dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex('title');
+
+    dataToSearch.addIndex('title');
+    dataToSearch.addIndex('tag_line');
+    dataToSearch.addIndex('authored_by');
+    dataToSearch.addIndex(['metadata', 'title']);
+    dataToSearch.addIndex(['metadata', 'industries']);
+    dataToSearch.addIndex(['metadata', 'programming_languages']);
+    dataToSearch.addIndex(['body_content', 'paragraphs']);
+
+    dataToSearch.addDocuments(stories);
+    this.setState({ 
+      stories,
+      search: dataToSearch, 
+      isLoading: false 
+    });
+  };
+
+  searchData = e => {
+    const searchQuery = e.target.value;
+    if (searchQuery) {
+      const queryResult = this.state.search.search(searchQuery);
+      this.setState({ 
+        searchQuery,
+        searchResults: queryResult 
+      });
+    } else {
+      this.setState({ 
+        searchQuery: '',
+        searchResults: [] 
+      });
+    }
+  };
+
+  handleSubmit = e => {
+    e.preventDefault();
+  };
+
+  render() {
+    const { searchResults, searchQuery, isLoading, isError } = this.state;
+    
+    if (isLoading) {
+      return <div className="text-center p-4">Loading search...</div>;
+    }
+
+    if (isError) {
+      return <div className="text-center p-4 text-danger">Error loading search</div>;
+    }
+
+    return (
+      <div className="container py-4 search-container">
+        <form onSubmit={this.handleSubmit} className="mb-4">
+          <div className="row justify-content-center">
+            <div className="col-md-8">
+              <div className="input-group">
+                <input
+                  id="Search"
+                  className="form-control form-control-lg"
+                  value={searchQuery}
+                  onChange={this.searchData}
+                  placeholder="Search user stories..."
+                />
+                <span className="input-group-text">
+                  <ion-icon name="search-outline"></ion-icon>
+                </span>
+              </div>
+            </div>
+          </div>
+        </form>
+
+        {searchQuery && (
+          <div>
+            <p className="text-bold mb-4">
+              Found {searchResults.length} {searchResults.length === 1 ? 'story' : 'stories'} 
+              &nbsp;for "{searchQuery}"
+            </p>
+
+            {searchResults.length > 0 ? (
+              <div className="stories-grid">
+                {searchResults.map((item, index) => (
+                  <Link 
+                    to={`/user-story/${item.slug}`} 
+                    key={`story_${index}`}
+                    className="story-card text-decoration-none"
+                  >
+                    <div className="card h-100">
+                      <div className="card-body">
+                        <h3 className="h5 card-title">{item.title}</h3>
+                        <p className="card-text text-muted">{item.tag_line}</p>
+                        <div className="story-meta mb-2">
+                          <small className="text-muted">
+                            By {item.authored_by} • {new Date(item.date).toLocaleDateString()}
+                          </small>
+                        </div>
+                        {item.metadata?.industries && (
+                          <div className="industries">
+                            {item.metadata.industries.map((industry, i) => (
+                              <span key={i} className="badge bg-light text-dark me-1">
+                                {industry}
+                              </span>
+                            ))}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                  </Link>
+                ))}
+              </div>
+            ) : (
+              <div className="text-center text-muted">
+                <p>No stories found matching your search.</p>
path: src/components/SearchContainer.jsx
user: login: krisstern
body: No worries
created_at: 2025-03-13T08:19:47Z
author_association: MEMBER

PR comments:
user: login: saurabhraghuvanshii
created_at: 2025-03-08T15:03:05Z
body: @krisstern sir we want to search stories? 
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-08T15:16:29Z
body: What do you mean? We want to index all of the pages and search everything on the website. 
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-08T15:56:25Z
body: check it sir , I think It done now
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-13T08:24:21Z
body: Thanks for the PR @saurabhraghuvanshii 
author_association: MEMBER
user: login: krisstern
created_at: 2025-03-13T08:26:19Z
body: My bad, your feature does not work in light mode:

<img width="1478" alt="Screenshot 2025-03-13 at 4 25 42 PM" src="https://github.com/user-attachments/assets/7f5a50b1-a8e8-4e88-bc4f-6f79278c7e60" />

Could you please fix it asap? 
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-13T08:27:23Z
body: Ya I will fix it sorry didn't see for light mode
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-13T08:30:46Z
body: Thanks! Appreciate it
author_association: MEMBER
user: login: saurabhraghuvanshii
created_at: 2025-03-13T08:42:47Z
body: sir how I switch for ligth theme I am not able to find way 
author_association: CONTRIBUTOR
user: login: krisstern
created_at: 2025-03-13T08:47:06Z
body: You will need to go to the Systems Settings of your machine and manually change the mode from dark to light mode. 
author_association: MEMBER
user: login: TenzDelek
created_at: 2025-03-13T09:14:30Z
body: <img width="1512" alt="Screenshot 2025-03-13 at 2 43 49 PM" src="https://github.com/user-attachments/assets/c132d74b-fdd7-4791-9557-06480bf62aa9" />

saw some overflow issue as well
author_association: CONTRIBUTOR
user: login: saurabhraghuvanshii
created_at: 2025-03-13T09:21:00Z
body: ya isee I also improving it 
author_association: CONTRIBUTOR
user: login: saurabhraghuvanshii
created_at: 2025-03-13T10:11:57Z
body: @krisstern #147 fix it .
author_association: CONTRIBUTOR