Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀 Replace State Pages #294

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
38 changes: 20 additions & 18 deletions components/ResourceCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { useRouter } from 'next/router';
import { getHaversineDistance } from '@lib/utils';
import Badge from './Badge';
import Description from './Description';

import FeedbackCounter from './FeedbackCounter';

const ResourceCard = ({ data, type: filterType, currentLocation }) => {
Expand All @@ -21,7 +20,7 @@ const ResourceCard = ({ data, type: filterType, currentLocation }) => {

// Metadata
const { external_id: id, last_verified_on, verification_status } = data;
const { upvotes, downvotes } = data;
const { upvotes, downvotes, data_name } = data;

// Oxygen Related Data
const { quantity_available, price } = data;
Expand All @@ -43,7 +42,7 @@ const ResourceCard = ({ data, type: filterType, currentLocation }) => {

const category = `${type}` + (resource_type ? ` - ${resource_type}` : '')

const directions = getGoogleMapsDirectionLink(latitude,longitude);
const directions = getGoogleMapsDirectionLink(latitude, longitude);

return (
<div id={id} className="max-w-3xl bg-white hover:bg-gray-100 dark:hover:bg-gray-1000 dark:bg-gray-1200 dark:text-gray-300 shadow-md rounded-md mx-2 md:mx-auto my-5 px-3 py-4">
Expand All @@ -64,7 +63,7 @@ const ResourceCard = ({ data, type: filterType, currentLocation }) => {
<div className="flex items-center dark:text-gray-500">
<FontAwesomeIcon icon={faMapMarkerAlt} className="w-5" />
<span className="ml-2 text-base xs:text-lg font-semibold">{district}</span>
{ directions.length > 0 &&
{directions.length > 0 &&
<a className="ml-2" target="_blank" href={directions}>
<button
type="button"
Expand Down Expand Up @@ -167,10 +166,13 @@ const ResourceCard = ({ data, type: filterType, currentLocation }) => {
</div>
}
{
source_link &&
<div className="flex items-center justify-end mt-1 w-1/2 xs:w-auto">
<div className="flex items-center justify-end text-right mt-1 w-1/2 xs:w-auto">
<FontAwesomeIcon icon={faLink} className="w-5" />
<a href={source_link} className="ml-1 text-base xs:text-lg">Source</a>
{
source_link ?
<a href={source_link} className="ml-1 text-base xs:text-md">{data_name}</a>
: <span className="ml-1 text-base xs:text-md">{data_name}</span>
}
</div>
}
</div>
Expand All @@ -191,18 +193,18 @@ const ResourceCard = ({ data, type: filterType, currentLocation }) => {
{/* <FeedbackCounter upvotes={upvotes} downvotes={downvotes} /> */}
</div>
<div className="flex items-center mx-1 mt-2 xs:my-0 xs:space-x-2">
{ currentLocation && latitude && longitude && (
<span className="text-xs mt-2 xs:my-0">
<span className="text-secondary-400 dark:text-primary-300">Approximately</span>
<span className="font-bold">
&nbsp;{getHaversineDistance(currentLocation, {
lat: latitude,
lng: longitude
})} Kms
{currentLocation && latitude && longitude && (
<span className="text-xs mt-2 xs:my-0">
<span className="text-secondary-400 dark:text-primary-300">Approximately</span>
<span className="font-bold">
&nbsp;{getHaversineDistance(currentLocation, {
lat: latitude,
lng: longitude
})} Kms
</span>
<span className="text-secondary-400 dark:text-primary-300">&nbsp;from your location</span>
</span>
<span className="text-secondary-400 dark:text-primary-300">&nbsp;from your location</span>
</span>
)}
)}
</div>
</div>
</div>
Expand Down
75 changes: 75 additions & 0 deletions components/StateHome.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { faCheckCircle, faFire, faGlobeAsia } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { statesAndDistrict } from '@lib/api';
import { getStatsByState } from '@lib/search';
import React, { useState } from 'react';
import { resources } from './search/SearchIntro';
import StateSearchField from './search/StateSearchField';

const StateHome = ({ state, type }) => {

const [resource, setResource] = useState(type || "All")

const statesWithDistricts = statesAndDistrict();
const states = Object.keys(statesWithDistricts);
state = states.find((e) => e.toLowerCase() === state?.toLowerCase());

const stats = getStatsByState(state)

const resourceListing = [
{ name: "All", icon: faGlobeAsia, count: stats["total"], verified: stats["verified"] },
...resources.map(e => ({ ...e, count: stats[e.name.toLowerCase()], verified: stats[`${e.name.toLowerCase()}_verified`] }))
];

return (
<main className="max-w-6xl mx-auto py-5">
<h1 className="font-semibold text-xl flex justify-center sm:justify-start pl-2">
<span>Resources in {state}</span>
</h1>
<div className="my-5 w-full">
<StateSearchField
state={state}
resource={resource}
setResource={setResource}
/>
</div>
<h1 className="font-semibold text-xl flex justify-center sm:justify-start pl-2 mb-3">
<span>Overview</span>
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
{
resourceListing.map(({ name, icon, verified, count }) => {
return (
<div key={name} className="shadow-md rounded-md px-5 py-3 flex justify-around mx-2">
<header className="flex flex-col justify-center items-center text-indigo-800">
<span className="mr-1 h-12 w-12 rounded-full justify-center flex items-center bg-gray-100 text-indigo-600">
<FontAwesomeIcon icon={icon} className="w-5 h-5" />
</span>
<span className="text-sm font-semibold">{name}</span>
</header>
<main className="flex flex-col my-2 ml-5 flex-1">
<div className="flex items-center py-1 justify-end">
<span className="mr-2 text-yellow-600">
<FontAwesomeIcon icon={faFire} className="w-3 h-3" />
</span>
<span>Available: </span>
<span className="ml-1 font-bold">{count}</span>
</div>
<div className="flex items-center py-1 justify-end">
<span className="mr-2 text-yellow-700">
<FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" />
</span>
<span>Verified: </span>
<span className="ml-1 font-bold">{verified}</span>
</div>
</main>
</div>
)
})
}
</div>
</main>
);
}

export default StateHome;
6 changes: 3 additions & 3 deletions components/search/SearchField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PulseIcon from "@components/icons/PulseIcon";
import { useRouter } from "next/router";
import { parametreize } from "@lib/utils";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog, faTimes } from "@fortawesome/free-solid-svg-icons";
import { faCog, faGlobeAsia, faTimes } from "@fortawesome/free-solid-svg-icons";
import { faTwitter } from "@fortawesome/free-brands-svg-icons";
import { resources } from "./SearchIntro";

Expand Down Expand Up @@ -58,7 +58,7 @@ const SearchField = ({ isFocus, onFocus, resource, setResource }) => {
return (
<div className="m-5">
<div className="flex items-center justify-around flex-wrap my-2 font-semibold">
{resources.map(({ name, icon }) =>
{[{ name: "All", icon: faGlobeAsia }, ...resources].map(({ name, icon }) =>
<div
onClick={(_) => handleTabChange(name)}
className={"cursor-pointer dark:text-gray-500 px-2 pt-1 border-primary-600 pb-1 text-center" + (name === resource ? " border-b-2 dark:text-primary-500 text-indigo-800" : "")}
Expand Down Expand Up @@ -91,7 +91,7 @@ const SearchField = ({ isFocus, onFocus, resource, setResource }) => {
value={searchText}
type="text"
className="p-4 pl-6 text-base md:text-xl transition-shadow duration-300 ease-in-out shadow-md hover:shadow-lg focus:shadow-xl placeholder-gray-600 dark:text-white rounded-xl z-10 outline-none w-full absolute top-0 left-0 bg-transparent"
placeholder={`Search for ${resource} in States or Districts`}
placeholder={`Search for ${resource} resources in States or Districts`}
/>
{
!searchText &&
Expand Down
141 changes: 141 additions & 0 deletions components/search/StateSearchField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useState, createRef } from "react";
import { isTrendingPlace, getSuggestedListByState, getSuggestedWordByState } from "@lib/search";
import TrendingIcon from "@components/icons/TrendingIcon";
import { useRouter } from "next/router";
import { parametreize } from "@lib/utils";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog, faGlobeAsia, faTimes } from "@fortawesome/free-solid-svg-icons";
import { resources } from "./SearchIntro";

const StateSearchField = ({ resource, setResource, state }) => {
const [searchText, setSearchText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isFocus, onFocus] = useState(false);
const suggestionText = getSuggestedWordByState(searchText, state);
const suggestedResults = getSuggestedListByState(searchText, state, resource);
const searchFieldRef = createRef();
const pageRouter = useRouter();

const handleSearchKeyDown = e => {
if (e.key === "Tab") {
e.preventDefault();
if (suggestionText.name) {
setSearchText(suggestionText.name);
}
} else if (e.key === "Escape") {
e.preventDefault();
searchFieldRef.current?.blur();
} else if (e.key === "Enter" && suggestionText?.name) {
handleGotoResource(suggestionText)
}
};

const handleGotoResource = (result) => {
setIsLoading(true);
setSearchText(result.name);
const { name, type, state } = result;
if (type === "District") {
pageRouter.push(
`/${parametreize(state)}/${parametreize(name)}/${parametreize(resource)}`
)
}
else if (type === "State") {
pageRouter.push(`/${parametreize(name)}/?resource=${resource}`)
}

}

const handleTabChange = (name) => {
setResource(name)
setTimeout(() => {
document.getElementById("searchField")?.focus();
}, 500)
}

return (
<div className="m-5">
<div className="flex items-center justify-around flex-wrap my-2 font-semibold">
{[{ name: "All", icon: faGlobeAsia }, ...resources].map(({ name, icon }) =>
<div
onClick={(_) => handleTabChange(name)}
className={"flex items-center cursor-pointer dark:text-gray-500 px-2 pt-1 border-primary-600 pb-1 text-center" + (name === resource ? " border-b-2 dark:text-primary-500 text-indigo-800" : "")}
key={name}>
<FontAwesomeIcon icon={icon} className="w-4 mr-1" />
<span>{name}</span>
</div>
)}
</div>
<div className={"my-2 relative w-full " + (isLoading && "animate-pulse")} >
<input
type="text"
readOnly={true}
className="p-4 pl-6 text-base bg-white dark:bg-gray-1000 md:text-xl placeholder-gray-600 rounded-xl outline-none w-full z-0"
placeholder={suggestionText.displayText}
/>
<input
id="searchField"
disabled={isLoading}
ref={searchFieldRef}
autoComplete="off"
onKeyDown={handleSearchKeyDown}
onFocus={_ => onFocus(true)}
onChange={({ target: { value } }) => setSearchText(value)}
onBlur={_ => {
setTimeout(() => {
onFocus(false);
}, 200)
}}
value={searchText}
type="text"
className="p-4 pl-6 text-base md:text-xl transition-shadow duration-300 ease-in-out shadow-md hover:shadow-lg focus:shadow-xl placeholder-gray-600 dark:text-white rounded-xl z-10 outline-none w-full absolute top-0 left-0 bg-transparent"
placeholder={`Search for districts with ${resource} resources`}
/>
{/* I am using Custom Close Icon instead of input[type='search'] to use dark style */}
{
(searchText && !isLoading) &&
<span className="absolute top-0 right-0 z-20 m-4 md:m-5" onClick={(_) => setSearchText("")}>
<FontAwesomeIcon icon={faTimes} className="w-3 h-3 dark:text-primary-400 text-secondary-600" />
</span>
}
{/* Loading Icon when the user makes use of suggestions */}
{
isLoading &&
<span className="absolute top-0 right-0 z-20 m-4 md:m-5" onClick={(_) => setSearchText("")}>
<FontAwesomeIcon icon={faCog} className="w-3 h-3 dark:text-primary-400 text-secondary-600" spin />
</span>
}
</div>
{isFocus && (
<div className="mt-8 px-2">
<h2 className="text-gray-600 text-sm">
{!suggestedResults.length
? "We couldn't find suggestions for you.. 💤"
: "Suggestions ⚡"}
</h2>
<ul
className="grid grid-cols-1 gap-1 sm:grid-cols-2 justify-center pt-3">
{suggestedResults.map((result, id) => (
<li
key={`${result.name}${id}`}
onClick={(_) => handleGotoResource(result)}
className="py-2 px-1 flex mt-1 bg-gray-200 dark:bg-gray-1200 hover:bg-gray-300 dark:hover:bg-gray-1000 cursor-pointer rounded-lg items-center justify-between">
<div className="flex items-center text-gray-500">
{isTrendingPlace(result.name) && (
<TrendingIcon className="h-5 w-5 text-red-600" />
)}
<span className="font-normal text-lg ml-1 text-gray-600 dark:text-gray-400">
{result.name}
</span>
</div>
<span className="font-semibold text-xs py-1 px-3 h-min rounded-full bg-gray-200 dark:bg-gray-1000 text-red-800 dark:text-red-300">
{result.state || result.type}
</span>
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default StateSearchField;
44 changes: 43 additions & 1 deletion lib/search.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import states from '@data/states.json';
import resourceStats from "@data/resource_stats_v2.json"
import topTenStatesAndDistricts from "@data/top_states_and_districts"

export const getAllStateNames = () => {
Expand All @@ -25,7 +26,7 @@ export const getAllDistrictNames = () => {
};

export const getSuggestedWord = searchText => {
if (!searchText) return "";
if (!searchText) return { displayText: "" };
const states = getAllStateNames();
const districts = getAllDistrictNames();
const stateSuggestion = states.find(e => isExactMatch(e, searchText));
Expand Down Expand Up @@ -74,3 +75,44 @@ export const getSuggestedList = searchText => {
export const isTrendingPlace = name => {
return topTenStatesAndDistricts.find(e => e.name === name);
};

export const getStatsByState = (state) => {
const data = resourceStats.states?.find(e => e.name === state && e.type === "State");
return data || {}
}

export const getSuggestedWordByState = (searchText, state) => {
if (!searchText) return { displayText: "" };
const allDistricts = resourceStats.districts.filter(e => e.state === state);
const districtSuggestion = allDistricts.find(e => isExactMatch(e, searchText));
if (districtSuggestion) {
const word = {
...districtSuggestion,
displayText:
searchText +
districtSuggestion.name?.substring(
searchText.length,
districtSuggestion.length
)
};
return word;
}
return { displayText: "" };
};

export const getSuggestedListByState = (searchText, state, type) => {
const RESULT_LIMIT = 10;

const allDistricts = resourceStats.districts.filter(e => e.state === state);

type = type === "All" ? "total" : type?.toLowerCase()

if (!searchText)
return allDistricts?.sort((a, b) => a[type] > b[type]).slice(0, RESULT_LIMIT);

const districts = allDistricts.filter(e => isPartialMatch(e, searchText));

return districts
.sort((a, _) => (isTrendingPlace(a.name) || isExactMatch(a, searchText) ? -1 : 0))
.slice(0, RESULT_LIMIT);
};
Loading