From d5ee97c5ed923055e3ea8c2b64393cc40548d466 Mon Sep 17 00:00:00 2001 From: myHerb <137535445+myHerbDev@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:12:24 +0300 Subject: [PATCH] Build modern myHerb sustainability website prototype --- README.md | 50 +++++ apps/myherb_shift_advisor.py | 256 ++++++++++++++++++++++ apps/test_myherb_shift_advisor.py | 91 ++++++++ apps/web/index.html | 129 +++++++++++ apps/web/script.js | 100 +++++++++ apps/web/styles.css | 346 ++++++++++++++++++++++++++++++ 6 files changed, 972 insertions(+) create mode 100644 apps/myherb_shift_advisor.py create mode 100644 apps/test_myherb_shift_advisor.py create mode 100644 apps/web/index.html create mode 100644 apps/web/script.js create mode 100644 apps/web/styles.css diff --git a/README.md b/README.md index 0880653..7ae5d2d 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,53 @@ By designing software with sustainability as a guiding principle, developers can DevSphere invites developers from diverse backgrounds and regions to join the initiative and actively participate in the shared mission of fostering sustainable software development. Together, the community can create a significant and positive impact on the environment, advocate for a more sustainable future within the software industry, and inspire others to embrace similar practices. Let us unite our skills and expertise to develop software for a greener tomorrow. 🌿vvvvvvvvvvvvvvvvvvvv + +## 🌿 myHerb Sustainability Shift Guidance (Prototype) + +To help founders move from sustainability ambition to measurable action, DevSphere now includes a lightweight prototype in `apps/myherb_shift_advisor.py`. + +### What it does +- Scores six core sustainability pillars (energy, water, waste, packaging, supply chain, community). +- Produces a weighted sustainability maturity score. +- Classifies organizations into a maturity tier (`Starter`, `Emerging`, `Progressing`, `Leader`). +- Suggests top 3 priority areas for the next sustainability shift sprint. + +### Quick run +```bash +python apps/myherb_shift_advisor.py +``` + +### Watch a sustainability journey (quarter-by-quarter) +```bash +python apps/myherb_shift_advisor.py --watch +``` +Use `--interval` to control playback speed and `--loop` for continuous mode: +```bash +python apps/myherb_shift_advisor.py --watch --interval 0.5 --loop +``` + +### Try it with your own scores (interactive) +```bash +python apps/myherb_shift_advisor.py --interactive +``` + +### Run tests +```bash +cd apps && python -m unittest test_myherb_shift_advisor.py +``` + +## 🌐 myHerb Website Experience (Modern Minimal Prototype) + +A new polished front-end experience is available at `apps/web/` to present myHerb as a professional, data-driven sustainability guidance platform. + +### Highlights +- Modern and minimal Google-like aesthetic with soft gradients and clear typography. +- Lightweight animations (floating insight card, dynamic score transitions, ambient glow). +- Interactive sustainability assessment form with instant tier + recommendation output. +- Responsive layout for desktop and mobile. + +### Run locally +```bash +python -m http.server 8080 -d apps/web +``` +Then open: `http://localhost:8080` diff --git a/apps/myherb_shift_advisor.py b/apps/myherb_shift_advisor.py new file mode 100644 index 0000000..df6736d --- /dev/null +++ b/apps/myherb_shift_advisor.py @@ -0,0 +1,256 @@ +"""myHerb Sustainability Shift Guidance Prototype. + +A lightweight CLI module to assess sustainability maturity and recommend +prioritized next steps. +""" + +from __future__ import annotations + +import argparse +import time +from dataclasses import dataclass +from typing import Callable, Dict, Iterable, List + + +PILLARS = ( + "energy", + "water", + "waste", + "packaging", + "supply_chain", + "community", +) + + +@dataclass(frozen=True) +class PillarAssessment: + """A maturity score for a sustainability pillar (0-100).""" + + name: str + score: int + + def __post_init__(self) -> None: + if self.name not in PILLARS: + raise ValueError(f"Unknown pillar: {self.name}") + if not 0 <= self.score <= 100: + raise ValueError("Score must be in the range 0..100") + + +@dataclass +class ShiftReport: + weighted_score: float + tier: str + priorities: List[str] + + +def _default_weights() -> Dict[str, float]: + return { + "energy": 0.20, + "water": 0.15, + "waste": 0.20, + "packaging": 0.15, + "supply_chain": 0.20, + "community": 0.10, + } + + +def _tier_from_score(score: float) -> str: + if score >= 80: + return "Leader" + if score >= 60: + return "Progressing" + if score >= 40: + return "Emerging" + return "Starter" + + +def generate_shift_report( + assessments: List[PillarAssessment], + weights: Dict[str, float] | None = None, +) -> ShiftReport: + """Generate a weighted sustainability shift report.""" + + if len(assessments) != len(PILLARS): + raise ValueError("All pillars must be assessed exactly once") + + seen = {assessment.name for assessment in assessments} + if seen != set(PILLARS): + raise ValueError("Assessments must include each pillar exactly once") + + resolved_weights = weights or _default_weights() + if set(resolved_weights) != set(PILLARS): + raise ValueError("Weights must include all pillars") + + total_weight = sum(resolved_weights.values()) + if abs(total_weight - 1.0) > 1e-9: + raise ValueError("Weights must sum to 1.0") + + score_map = {assessment.name: assessment.score for assessment in assessments} + weighted_score = sum(score_map[p] * resolved_weights[p] for p in PILLARS) + tier = _tier_from_score(weighted_score) + + underperforming = sorted(PILLARS, key=lambda p: score_map[p])[:3] + priorities = [ + f"Increase {pillar.replace('_', ' ')} initiatives (current score: {score_map[pillar]})" + for pillar in underperforming + ] + + return ShiftReport( + weighted_score=round(weighted_score, 2), + tier=tier, + priorities=priorities, + ) + + +def render_report(title: str, report: ShiftReport) -> str: + """Render a report as a console-friendly text block.""" + + priorities = "\n".join(f"- {item}" for item in report.priorities) + return ( + f"\n{title}\n" + f"Weighted Score: {report.weighted_score}\n" + f"Tier: {report.tier}\n" + "Top priorities:\n" + f"{priorities}\n" + ) + + +def _assessments_from_scores(scores: Dict[str, int]) -> List[PillarAssessment]: + return [PillarAssessment(name=pillar, score=scores[pillar]) for pillar in PILLARS] + + +def demo() -> ShiftReport: + """Return a sample report for showcasing the application idea.""" + + sample_scores = { + "energy": 55, + "water": 62, + "waste": 48, + "packaging": 45, + "supply_chain": 52, + "community": 70, + } + return generate_shift_report(_assessments_from_scores(sample_scores)) + + +def watch_sample_shift() -> Iterable[str]: + """Yield a simple quarter-by-quarter sustainability journey to watch progress.""" + + snapshots = [ + ( + "Quarter 1 (Baseline)", + { + "energy": 45, + "water": 54, + "waste": 40, + "packaging": 42, + "supply_chain": 47, + "community": 58, + }, + ), + ( + "Quarter 2 (Pilot Programs)", + { + "energy": 58, + "water": 60, + "waste": 52, + "packaging": 56, + "supply_chain": 54, + "community": 65, + }, + ), + ( + "Quarter 3 (Scale-Up)", + { + "energy": 68, + "water": 71, + "waste": 64, + "packaging": 66, + "supply_chain": 63, + "community": 72, + }, + ), + ] + + for label, scores in snapshots: + report = generate_shift_report(_assessments_from_scores(scores)) + yield render_report(f"myHerb Sustainability Shift Guidance - {label}", report) + + +def run_watch_mode( + interval_seconds: float = 1.5, + loop_forever: bool = False, + emit: Callable[[str], None] = print, + sleeper: Callable[[float], None] = time.sleep, +) -> None: + """Stream watch snapshots with a configurable delay.""" + + if interval_seconds < 0: + raise ValueError("Watch interval must be >= 0") + + while True: + for block in watch_sample_shift(): + emit(block) + sleeper(interval_seconds) + if not loop_forever: + return + + +def run_interactive_assessment() -> ShiftReport: + """Collect scores from a user in the terminal and generate a report.""" + + print("Enter your score for each pillar (0-100).") + scores: Dict[str, int] = {} + for pillar in PILLARS: + while True: + raw = input(f"{pillar.replace('_', ' ').title()}: ").strip() + try: + score = int(raw) + assessment = PillarAssessment(name=pillar, score=score) + scores[pillar] = assessment.score + break + except ValueError as error: + print(f"Invalid input: {error}") + + return generate_shift_report(_assessments_from_scores(scores)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="myHerb Sustainability Shift Guidance") + parser.add_argument( + "--watch", + action="store_true", + help="Watch a quarter-by-quarter demo progression.", + ) + parser.add_argument( + "--interactive", + action="store_true", + help="Enter your own pillar scores in the terminal.", + ) + parser.add_argument( + "--interval", + type=float, + default=1.5, + help="Delay (seconds) between watch snapshots. Used with --watch.", + ) + parser.add_argument( + "--loop", + action="store_true", + help="Repeat watch snapshots continuously. Used with --watch.", + ) + args = parser.parse_args() + + if args.watch: + run_watch_mode(interval_seconds=args.interval, loop_forever=args.loop) + return + + if args.interactive: + report = run_interactive_assessment() + print(render_report("myHerb Sustainability Shift Guidance - Your Snapshot", report)) + return + + print(render_report("myHerb Sustainability Shift Guidance - Demo", demo())) + + +if __name__ == "__main__": + main() diff --git a/apps/test_myherb_shift_advisor.py b/apps/test_myherb_shift_advisor.py new file mode 100644 index 0000000..85668dd --- /dev/null +++ b/apps/test_myherb_shift_advisor.py @@ -0,0 +1,91 @@ +import unittest + +from myherb_shift_advisor import ( + PillarAssessment, + generate_shift_report, + render_report, + run_watch_mode, + watch_sample_shift, +) + + +class ShiftAdvisorTests(unittest.TestCase): + def test_report_generation(self): + assessments = [ + PillarAssessment("energy", 80), + PillarAssessment("water", 70), + PillarAssessment("waste", 60), + PillarAssessment("packaging", 65), + PillarAssessment("supply_chain", 75), + PillarAssessment("community", 85), + ] + + report = generate_shift_report(assessments) + + self.assertEqual(report.tier, "Progressing") + self.assertAlmostEqual(report.weighted_score, 71.75) + self.assertEqual(len(report.priorities), 3) + + def test_invalid_weight_sum(self): + assessments = [ + PillarAssessment("energy", 80), + PillarAssessment("water", 70), + PillarAssessment("waste", 60), + PillarAssessment("packaging", 65), + PillarAssessment("supply_chain", 75), + PillarAssessment("community", 85), + ] + + with self.assertRaises(ValueError): + generate_shift_report( + assessments, + { + "energy": 0.1, + "water": 0.1, + "waste": 0.1, + "packaging": 0.1, + "supply_chain": 0.1, + "community": 0.1, + }, + ) + + def test_render_report_contains_basics(self): + assessments = [ + PillarAssessment("energy", 80), + PillarAssessment("water", 70), + PillarAssessment("waste", 60), + PillarAssessment("packaging", 65), + PillarAssessment("supply_chain", 75), + PillarAssessment("community", 85), + ] + report = generate_shift_report(assessments) + output = render_report("Test", report) + + self.assertIn("Weighted Score", output) + self.assertIn("Top priorities", output) + + def test_watch_mode_has_three_snapshots(self): + snapshots = list(watch_sample_shift()) + + self.assertEqual(len(snapshots), 3) + self.assertIn("Quarter 1", snapshots[0]) + self.assertIn("Quarter 3", snapshots[-1]) + + def test_run_watch_mode_emits_all_snapshots(self): + seen = [] + run_watch_mode( + interval_seconds=0, + loop_forever=False, + emit=seen.append, + sleeper=lambda _: None, + ) + + self.assertEqual(len(seen), 3) + + def test_run_watch_mode_rejects_negative_interval(self): + with self.assertRaises(ValueError): + run_watch_mode(interval_seconds=-0.5, sleeper=lambda _: None) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..114ba5c --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,129 @@ + + + + + + + myHerb | Sustainability Shift Guidance + + + + + + + + + +
+ + myHerb + + + +
+ +
+
+
+

Data-driven sustainability shift guidance

+

Build a greener business strategy with confidence.

+

+ myHerb blends measurable sustainability analytics with practical, + prioritized action plans. Minimal interface, maximum clarity. +

+ +
    +
  • 6Core pillars
  • +
  • 3Top priorities per sprint
  • +
  • <2mTo receive first guidance
  • +
+
+ +
+ +
+

Why teams choose myHerb

+
+
+

Actionable intelligence

+

+ Translate scores into immediate decisions: what to fix now, what to + scale next, and what to monitor. +

+
+
+

Founder-ready simplicity

+

+ Designed in a clean, Google-like style that keeps complex + sustainability data understandable for every stakeholder. +

+
+
+

Guidance that inspires

+

+ Prioritized recommendations and progress storytelling help teams + maintain momentum and attract mission-aligned visitors. +

+
+
+
+ +
+
+

Quick sustainability assessment

+

Set each pillar from 0–100 and get your tailored shift guidance.

+
+ +
+ + + + + + + +
+ +
+

Recommendations

+

Submit your assessment to view recommendations.

+
+
+
+ + + + + + diff --git a/apps/web/script.js b/apps/web/script.js new file mode 100644 index 0000000..80b22ce --- /dev/null +++ b/apps/web/script.js @@ -0,0 +1,100 @@ +const PILLARS = [ + "energy", + "water", + "waste", + "packaging", + "supply_chain", + "community", +]; + +const WEIGHTS = { + energy: 0.2, + water: 0.15, + waste: 0.2, + packaging: 0.15, + supply_chain: 0.2, + community: 0.1, +}; + +const tierFromScore = (score) => { + if (score >= 80) return "Leader"; + if (score >= 60) return "Progressing"; + if (score >= 40) return "Emerging"; + return "Starter"; +}; + +const toNumber = (value) => { + const parsed = Number(value); + if (Number.isNaN(parsed)) return 0; + return Math.max(0, Math.min(100, parsed)); +}; + +const computeReport = (scores) => { + const weightedScore = PILLARS.reduce( + (sum, pillar) => sum + toNumber(scores[pillar]) * WEIGHTS[pillar], + 0, + ); + + const ordered = [...PILLARS].sort((a, b) => scores[a] - scores[b]).slice(0, 3); + + const priorities = ordered.map( + (pillar) => + `Increase ${pillar.replace("_", " ")} initiatives (current score: ${scores[pillar]})`, + ); + + return { + weightedScore: Number(weightedScore.toFixed(2)), + tier: tierFromScore(weightedScore), + priorities, + }; +}; + +const scoreNode = document.getElementById("scoreNumber"); +const tierNode = document.getElementById("tierBadge"); +const progressNode = document.getElementById("progressBar"); +const resultNode = document.getElementById("result"); +const formNode = document.getElementById("assessmentForm"); + +document.getElementById("year").textContent = new Date().getFullYear(); + +const animateScore = (target) => { + const start = Number(scoreNode.textContent) || 0; + const durationMs = 700; + const begin = performance.now(); + + const tick = (now) => { + const progress = Math.min(1, (now - begin) / durationMs); + const current = start + (target - start) * progress; + scoreNode.textContent = current.toFixed(1); + if (progress < 1) requestAnimationFrame(tick); + }; + + requestAnimationFrame(tick); +}; + +const renderResult = (report) => { + const list = report.priorities.map((item) => `
  • ${item}
  • `).join(""); + resultNode.innerHTML = ` +

    Your guidance output

    +

    Tier: ${report.tier}  |  Score: ${report.weightedScore}

    +

    Top 3 sustainability shift priorities:

    + + `; + + tierNode.textContent = report.tier; + progressNode.style.width = `${report.weightedScore}%`; + animateScore(report.weightedScore); +}; + +formNode.addEventListener("submit", (event) => { + event.preventDefault(); + const formData = new FormData(formNode); + const scores = Object.fromEntries(PILLARS.map((pillar) => [pillar, toNumber(formData.get(pillar))])); + renderResult(computeReport(scores)); +}); + +window.addEventListener("load", () => { + const initialData = new FormData(formNode); + const scores = Object.fromEntries(PILLARS.map((pillar) => [pillar, toNumber(initialData.get(pillar))])); + renderResult(computeReport(scores)); +}); diff --git a/apps/web/styles.css b/apps/web/styles.css new file mode 100644 index 0000000..e437b6f --- /dev/null +++ b/apps/web/styles.css @@ -0,0 +1,346 @@ +:root { + --bg: #f8fbfb; + --card: rgba(255, 255, 255, 0.72); + --text: #1f2933; + --muted: #5f6c75; + --brand: #6eaeb0; + --brand-dark: #4a8f92; + --ring: rgba(110, 174, 176, 0.33); + --border: rgba(145, 162, 171, 0.22); + --shadow: 0 20px 45px rgba(48, 68, 76, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + color: var(--text); + background: radial-gradient(circle at top right, #eef8f7 0%, var(--bg) 65%); + line-height: 1.6; + overflow-x: hidden; +} + +.container { + width: min(1120px, calc(100% - 2rem)); + margin: 0 auto; +} + +.bg-glow { + position: fixed; + width: 30rem; + height: 30rem; + border-radius: 50%; + filter: blur(70px); + opacity: 0.35; + z-index: -1; + pointer-events: none; +} + +.bg-glow--one { + background: #b8ece8; + top: -14rem; + right: -7rem; + animation: pulse 9s ease-in-out infinite; +} + +.bg-glow--two { + background: #d7d3ff; + bottom: -14rem; + left: -10rem; + animation: pulse 11s ease-in-out infinite reverse; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.1rem 0; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.4rem; + text-decoration: none; + color: inherit; +} + +.brand__word { + font-weight: 700; + font-size: clamp(1.2rem, 2vw, 1.7rem); + letter-spacing: -0.02em; +} + +.brand__word em { + font-style: italic; + font-weight: 500; + color: #7b6a56; +} + +.brand__leaf { + width: 2.45rem; + fill: var(--brand); +} + +.topnav { + display: inline-flex; + gap: 1rem; +} + +.topnav a { + text-decoration: none; + color: var(--muted); + font-weight: 500; +} + +.hero { + display: grid; + grid-template-columns: 1.15fr minmax(260px, 380px); + gap: 2rem; + align-items: center; + padding: 2.3rem 0 1.4rem; +} + +.eyebrow { + display: inline-block; + padding: 0.3rem 0.7rem; + border-radius: 999px; + background: rgba(110, 174, 176, 0.13); + color: var(--brand-dark); + font-weight: 600; + font-size: 0.86rem; + margin-bottom: 0.8rem; +} + +h1, +h2, +h3 { + line-height: 1.2; + margin: 0 0 0.6rem; +} + +h1 { + font-size: clamp(2rem, 4.6vw, 3.4rem); + letter-spacing: -0.03em; +} + +.lead { + font-size: 1.05rem; + color: var(--muted); + max-width: 58ch; +} + +.hero__actions { + margin: 1.3rem 0 1.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.8rem; +} + +.btn { + border: 0; + text-decoration: none; + display: inline-flex; + justify-content: center; + align-items: center; + padding: 0.73rem 1rem; + border-radius: 0.8rem; + font-weight: 600; + cursor: pointer; +} + +.btn--primary { + background: linear-gradient(140deg, var(--brand-dark), var(--brand)); + color: #fff; + box-shadow: 0 12px 24px rgba(74, 143, 146, 0.2); +} + +.btn--ghost { + background: transparent; + color: var(--brand-dark); + border: 1px solid var(--ring); +} + +.metrics { + margin: 0; + padding: 0; + list-style: none; + display: flex; + gap: 1.6rem; +} + +.metrics li { + display: grid; +} + +.metrics strong { + font-size: 1.45rem; +} + +.metrics span { + color: var(--muted); + font-size: 0.88rem; +} + +.hero-card, +.card, +.result, +.assess-grid { + background: var(--card); + border: 1px solid var(--border); + border-radius: 1.1rem; + backdrop-filter: blur(4px); +} + +.hero-card { + padding: 1.2rem; + box-shadow: var(--shadow); +} + +.hero-card__tier { + margin: 0; + font-size: 1.3rem; + font-weight: 700; +} + +.hero-card__score { + margin: 0.15rem 0 0.75rem; + font-size: 1rem; + color: var(--muted); +} + +.progress { + background: #edf3f4; + height: 0.58rem; + border-radius: 999px; + overflow: hidden; +} + +.progress span { + display: block; + height: 100%; + width: 67.4%; + border-radius: inherit; + background: linear-gradient(90deg, var(--brand), #8dd7d9); + transition: width 480ms ease; +} + +.section { + padding: 2rem 0; +} + +.cards { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.card { + padding: 1rem; +} + +.card p, +.section__head p, +.muted, +label { + color: var(--muted); +} + +.section__head { + margin-bottom: 1rem; +} + +.assess-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.8rem; + padding: 1rem; +} + +label { + display: grid; + font-size: 0.92rem; + gap: 0.35rem; +} + +input { + border: 1px solid var(--border); + border-radius: 0.6rem; + padding: 0.6rem 0.7rem; + font: inherit; +} + +input:focus { + outline: 2px solid var(--ring); + border-color: var(--brand); +} + +.result { + margin-top: 0.9rem; + padding: 1rem; +} + +.result ul { + margin: 0.45rem 0 0; + padding-left: 1.05rem; +} + +.footer { + padding: 1.4rem 0 2.1rem; + color: var(--muted); + font-size: 0.92rem; +} + +.float { + animation: floatCard 5.2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +@keyframes floatCard { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } +} + +@media (max-width: 960px) { + .hero { + grid-template-columns: 1fr; + } + + .cards, + .assess-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 620px) { + .topnav { + display: none; + } + + .cards, + .assess-grid { + grid-template-columns: 1fr; + } + + .metrics { + gap: 1rem; + flex-wrap: wrap; + } +}