← Mike Higgins Home
Custom Code Experiment •

Indonesian Greetings

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  <title>Indonesian Greetings</title>
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <script src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.development.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.development.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/4.1.0/immutable.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <style>
    * {
      position: relative;
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      background: #121212;
      font-family: 'Roboto', sans-serif;
      height: 100%;
      width: 100%;
    }

    * {
      font-family: 'Roboto', sans-serif;
      font-weight: 300;
    }

    .wrapper {
      width: 100%;
      height: 100vh;
      overflow-x: hidden;
    }

    .content-wrapper {
      display: flex;
      height: 93vh;
      justify-content: center;
      align-items: center;
      background: #121212;
      margin: 0 auto;
      padding: 0 2em;
    }

    .card {
      width: 30em;
      height: 15em;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #1e1e2f;
      box-shadow: 0 8px 24px rgba(0,0,0,0.5);
      padding: 1em 3em;
      transform-style: preserve-3d;
      margin-top: 4em;
      transition: 0.2s;
      cursor: pointer;
      border-radius: 20px;
      border: 1px solid #333340;
    }

    @media(max-width: 600px) {
      .card {
        width: 25em;
      }
    }

    @media(max-height: 823px) {
      .card {
        margin-top: 9em;
      }
    }

    .card__flip-card {
      position: absolute;
      top: 4%;
      right: 5%;
      cursor: pointer;
      color: #aaa;
      font-size: 1.2rem;
      transition: 0.2s;
      z-index: 5;
    }
    .card__flip-card:hover {
      color: #ddd;
    }

    .card__counter {
      position: absolute;
      left: 5%;
      top: 5%;
      font-size: 0.85rem;
      background: #2c2c3a;
      padding: 0.2rem 0.6rem;
      border-radius: 20px;
      color: #ccccdd;
      font-weight: 400;
    }

    .card__content--front {
      animation: fadeInUp 0.5s ease;
      font-weight: 500;
      font-size: 1.8rem;
      text-align: center;
      word-break: break-word;
      color: #f0f0f0;
      text-shadow: 0 1px 2px rgba(0,0,0,0.2);
    }

    .card__content--back {
      animation: fadeIn 0.6s ease;
      line-height: 1.5;
      font-size: 1rem;
      text-align: center;
      word-break: break-word;
      max-height: 90%;
      overflow-y: auto;
      color: #cfcfdf;
      padding: 0 0.2rem;
    }

    .header {
      background: #1a1a2a;
      box-shadow: 0 4px 16px rgba(0,0,0,0.4);
      padding: 1em 0em;
      display: flex;
      align-items: center;
      justify-content: center;
      height: 7vh;
      color: #e0e0e0;
      position: absolute;
      width: 100%;
      z-index: 10;
      top: 0;
      left: 0;
    }

    .header-content {
      width: 100%;
      display: flex;
      justify-content: center;
      font-weight: 400;
      font-size: 1.3rem;
      letter-spacing: 0.5px;
    }
    .header-content span {
      font-size: 1.1rem;
    }
    .header-content__left, .header-content__right {
      display: none;
    }

    .card-container__icon {
      display: none;
    }

    .card__actions {
      position: absolute;
      bottom: 6%;
      display: flex;
      width: 100%;
      justify-content: center;
      opacity: 0;
      transition: 0.3s;
      left: 0;
      gap: 0.75rem;
      pointer-events: none;
    }
    .card__actions.active {
      opacity: 1;
      pointer-events: auto;
    }
    .card__next-button, .card__prev-button {
      cursor: pointer;
      padding: 0.5rem 1rem;
      background: #2d2d3a;
      color: #eaeef5;
      transition: 0.2s;
      border-radius: 40px;
      font-weight: 400;
      font-size: 0.85rem;
      backdrop-filter: blur(2px);
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
      border: 1px solid #3a3a4a;
    }
    .card__next-button:hover, .card__prev-button:hover {
      background: #3e3e52;
      color: white;
    }

    .card-container__dots-wrapper {
      padding: 0.8rem 0rem;
      margin-top: 1rem;
      text-align: center;
      display: flex;
      justify-content: center;
      gap: 0.6rem;
    }
    .card-container__dot {
      color: #4a4a60;
      cursor: pointer;
      font-size: 0.8rem;
      transition: 0.2s;
    }
    .card-container__dot.active {
      color: #5f9eff;
      text-shadow: 0 0 2px #3f6bc0;
      transform: scale(1.2);
    }
    .card-container__dot:hover {
      color: #9bb8ff;
    }

    @keyframes fadeInUp {
      0% {
        opacity: 0;
        transform: translateY(20px);
      }
      100% {
        opacity: 1;
        transform: translateY(0);
      }
    }
    @keyframes fadeIn {
      0% {
        opacity: 0;
      }
      100% {
        opacity: 1;
      }
    }

    .create-card, .create-card__shadow, .create-card__body {
      display: none !important;
    }
  </style>
</head>
<body>
<div id='app'></div>

<script type="text/babel">
  class Header extends React.Component {
    render() {
      return (
        <div className='header'>
          <div className='header-content'>
            <span>🇮🇩 Bahasa Indonesia • Greetings</span>
          </div>
        </div>
      );
    }
  }

  class Card extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        showAnswer: false
      };
    }

    render() {
      const content = this.state.showAnswer ? this.props.backContent : this.props.frontContent;
      const iconClass = this.state.showAnswer ? 'reply' : 'share';
      const cardClass = this.state.showAnswer ? 'back' : '';
      const contentClass = this.state.showAnswer ? 'back' : 'front';
      const actionClass = this.state.showAnswer ? 'active' : '';

      const handleFlipClick = (e) => {
        e.stopPropagation();
        this.setState({ showAnswer: !this.state.showAnswer });
      };

      const handlePrev = (e) => {
        e.stopPropagation();
        this.props.showPrevCard();
        this.setState({ showAnswer: false });
      };

      const handleNext = (e) => {
        e.stopPropagation();
        this.props.showNextCard();
        this.setState({ showAnswer: false });
      };

      return (
        <div
          className={`card ${cardClass}`}
          onClick={() => this.setState({ showAnswer: !this.state.showAnswer })}
        >
          <span className='card__counter'>{this.props.cardNumber + 1}</span>
          <div
            className='card__flip-card'
            onClick={handleFlipClick}
          >
            <span className={`fa fa-${iconClass}`} />
          </div>
          <div className={`card__content--${contentClass}`}>
            {content}
          </div>
          <div className={`card__actions ${actionClass}`}>
            <div
              className='card__prev-button'
              onClick={handlePrev}
            >
              ← Prev
            </div>
            <div
              className='card__next-button'
              onClick={handleNext}
            >
              Next →
            </div>
          </div>
        </div>
      );
    }
  }

  class CardContainer extends React.Component {
    constructor(props) {
      super(props);
      const initialData = Immutable.fromJS([
        {
          word: 'Selamat Pagi',
          description: 'Good morning. 🇮🇩 Used from sunrise until about 11 AM. Polite and warm. Pronunciation: Se-la-mat Pa-gi (soft “t”).'
        },
        {
          word: 'Selamat Siang',
          description: 'Good day / Early afternoon. 🇮🇩 11 AM – 3 PM. Bright, formal greeting. Pronunciation: Se-la-mat See-ang.'
        },
        {
          word: 'Selamat Sore',
          description: 'Good afternoon. 🇮🇩 From 3 PM to sunset (around 6 PM). A pleasant late-day greeting. Pronunciation: Se-la-mat So-ray.'
        },
        {
          word: 'Selamat Malam',
          description: 'Good evening / Good night. 🇮🇩 After sunset (from 6 PM onward). Used to greet at night. Pronunciation: Se-la-mat Ma-lam.'
        },
        {
          word: 'Apa Kabar?',
          description: 'How are you? 🇮🇩 Literally “What’s the news?” A standard, caring question after greeting. Reply: “Kabar baik” (I’m fine). Pronunciation: Ah-pah Kah-bar.'
        },
        {
          word: 'Kabar Baik',
          description: 'I am fine / Good news. 🇮🇩 Positive response to “Apa Kabar?”. Also “Baik-baik saja” (just fine). Pronunciation: Kah-bar Ba-ik.'
        },
        {
          word: 'Mau ke mana?',
          description: 'Where are you going? 🇮🇩 Friendly cultural greeting, not invasive. Common reply: “Jalan-jalan saja” (just strolling). Pronunciation: Ma-oo ke mah-nah?'
        },
        {
          word: 'Terima Kasih',
          description: 'Thank you. 🇮🇩 Literally “receive love”. Essential politeness. Casual: “Makasih”. Pronunciation: Te-ree-mah Kah-see.'
        },
        {
          word: 'Sama-sama',
          description: 'You’re welcome. 🇮🇩 The standard, warm reply to “terima kasih”. Pronunciation: Sah-mah Sah-mah.'
        },
        {
          word: 'Hati-hati',
          description: 'Take care / Be careful. 🇮🇩 A kind farewell, especially when someone is travelling. Pronunciation: Hah-tee Hah-tee.'
        }
      ]);
      this.state = {
        cards: initialData,
        cardNumber: 0,
      };
      this.showPrevCard = this.showPrevCard.bind(this);
      this.showNextCard = this.showNextCard.bind(this);
    }

    showNextCard() {
      const nextIndex = this.state.cardNumber + 1;
      if (nextIndex < this.state.cards.size) {
        this.setState({ cardNumber: nextIndex });
      }
    }

    showPrevCard() {
      const prevIndex = this.state.cardNumber - 1;
      if (prevIndex >= 0) {
        this.setState({ cardNumber: prevIndex });
      }
    }

    generateDots() {
      const total = this.state.cards.size;
      const currentIdx = this.state.cardNumber;
      let dots = [];
      for (let i = 0; i < total; i++) {
        const dotClass = i === currentIdx ? 'active' : '';
        dots.push(
          <span
            key={`dot-${i}`}
            className={`card-container__dot fa fa-circle ${dotClass}`}
            onClick={() => this.setState({ cardNumber: i })}
          />
        );
      }
      return dots;
    }

    generateCurrentCard() {
      const cardsList = this.state.cards;
      if (!cardsList.size) return null;
      const currentCard = cardsList.get(this.state.cardNumber);
      if (!currentCard) return null;
      return (
        <Card
          key={this.state.cardNumber}
          frontContent={currentCard.get('word')}
          backContent={currentCard.get('description')}
          showNextCard={this.showNextCard}
          showPrevCard={this.showPrevCard}
          cardNumber={this.state.cardNumber}
        />
      );
    }

    render() {
      return (
        <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
          {this.generateCurrentCard()}
          <div className='card-container__dots-wrapper'>
            {this.generateDots()}
          </div>
        </div>
      );
    }
  }

  class Main extends React.Component {
    render() {
      return (
        <div className='wrapper'>
          <Header />
          <div className='content-wrapper'>
            <CardContainer />
          </div>
        </div>
      );
    }
  }

  ReactDOM.render(<Main />, document.getElementById('app'));
</script>
</body>
</html>