FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index FiveWin for Harbour/xHarbour HIX & AntiGravity apps gallery
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
HIX & AntiGravity apps gallery
Posted: Sun Dec 07, 2025 03:21 PM

chess.prg

// Chess Game Logic for HIX
// Uses FEN for state management

function Main()
   local oErr, cHtml
   

   BEGIN SEQUENCE
      cHtml := ChessGame()
   RECOVER USING oErr
      cHtml := "<h2>Error: " + ValToChar(oErr:Description) + "</h2>"
      cHtml += "<p>Operation: " + ValToChar(oErr:Operation) + "</p>"
      cHtml += "<p>Filename: " + ValToChar(oErr:Filename) + "</p>"
      cHtml += "<p>GenCode: " + ValToChar(oErr:GenCode) + "</p>"
      cHtml += "<p>SubCode: " + ValToChar(oErr:SubCode) + "</p>"
      cHtml += "<p>Args: " + ValToChar(oErr:Args) + "</p>"
   END
   

return cHtml

function ChessGame()
   local hPost := UPost()
   local cHtml := ""
   local cAction, hParams
   local cFEN, cSelected, cTurn
   local aValidMoves := {}
   local cClicked, cPiece, cTurnColor
   

   if hb_HHasKey( hPost, "action" )
      cAction := hPost["action"]
   else
      cAction := "new"
   endif

   if hb_HHasKey( hPost, "fen" )
      cFEN := hPost["fen"]
   else
      cFEN := InitBoard()
   endif

   if Empty( cFEN )
      cFEN := InitBoard()
   endif

   cSelected := ""
   if hb_HHasKey( hPost, "selected" )
      cSelected := hPost["selected"]
   endif

   do case
      case cAction == "new"
         cFEN := InitBoard()
         cSelected := ""
         

  case cAction == "select"
     
     cClicked := hPost["clicked_square"]
     
     if !Empty( cClicked ) 
         cPiece := GetPieceAt( cFEN, cClicked )
         cTurnColor := GetTurn( cFEN ) 
         
         if cClicked == cSelected
            cSelected := "" // Deselect
         else
            if !Empty(cSelected)
               // Try to move
               if IsValidMove( cFEN, cSelected, cClicked )
                  cFEN := MakeMove( cFEN, cSelected, cClicked )
                  cSelected := "" // Move done, deselect
               else
                  // Invalid move. 
                  // If clicked square has own piece, switch selection.
                  if IsOwnPiece( cPiece, cTurnColor )
                     cSelected := cClicked
                     aValidMoves := GetValidMoves( cFEN, cSelected )
                  else
                      // Invalid move to empty or enemy square, effectively deselect/reset
                      cSelected := ""
                  endif
               endif
            else
               // Nothing selected, selecting now
               if IsOwnPiece( cPiece, cTurnColor )
                  cSelected := cClicked
                  aValidMoves := GetValidMoves( cFEN, cSelected )
               endif
            endif
         endif
     endif
     
   endcase
   

   hParams := {;
      "fen" => cFEN, ;
      "board_html" => RenderBoard( cFEN, cSelected, aValidMoves ), ;
      "turn" => iif( GetTurn(cFEN) == "w", "White", "Black" ), ;
      "selected" => cSelected ;
   }
   

   cHtml := View( "chess", hParams )

return cHtml

//----------------------------------------------------------------------------//

function InitBoard()
   // Standard starting position FEN
   return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"

function GetTurn( cFEN )
   local aParts := hb_ATokens( cFEN, " " )
   if Len(aParts) >= 2
      return aParts[2]
   endif
return "w"

function GetPieceAt( cFEN, cSquare )
   local aGrid := FENToGrid( cFEN )
   local aCoords := SquareToCoords( cSquare )
   return aGrid[ aCoords[1], aCoords[2] ]

function IsOwnPiece( cPiece, cTurnColor )
   if Empty(cPiece); return .F.; endif
   if cTurnColor == "w"
      return IsUpperLocal( cPiece ) // White pieces are uppercase
   else
      return !IsUpperLocal( cPiece ) // Black pieces are lowercase
   endif
return .F.

//----------------------------------------------------------------------------//

function IsValidMove( cFEN, cFrom, cTo )
   local aMoves := GetValidMoves( cFEN, cFrom )
   local i
   for i := 1 to Len(aMoves)
      if aMoves[i] == cTo
         return .T.
      endif
   next
return .F.

function MakeMove( cFEN, cFrom, cTo )
   local aGrid := FENToGrid( cFEN )
   local aFrom := SquareToCoords( cFrom )
   local aTo := SquareToCoords( cTo )
   local cPiece := aGrid[ aFrom[1], aFrom[2] ]
   local cTurn := GetTurn( cFEN )
   local cNextTurn := iif( cTurn == "w", "b", "w" )
   

   // Move piece
   aGrid[ aTo[1], aTo[2] ] := cPiece
   aGrid[ aFrom[1], aFrom[2] ] := ""
   

   // TODO: Handle Pawn Promotion, Castling, En Passant logic updates
   // For now, simple movement
   

   return GridToFEN( aGrid ) + " " + cNextTurn + " KQkq - 0 1" // Simply toggle turn, preserve rights for now

//----------------------------------------------------------------------------//

function GetValidMoves( cFEN, cFrom )
   local aMoves := {}
   local aGrid := FENToGrid( cFEN )
   local aFrom := SquareToCoords( cFrom )
   local nRow := aFrom[1]
   local nCol := aFrom[2]
   local cPiece := aGrid[nRow, nCol]
   local cType := Upper( cPiece )
   local cColor := iif( IsUpperLocal(cPiece), "w", "b" )
   local i, j, r, c
   local nDir, nStartRow, nTemp
   local aCapCols, aOffs, aDirs
   

   if Empty(cPiece); return {}; endif
   

   // Simplified Move Logic (No check validation yet)
   do case
      case cType == "P" // Pawn
         nDir := iif( cColor == "w", -1, 1 ) // White moves up (row decrements)
         nStartRow := iif( cColor == "w", 7, 2 )
         

     // Move forward 1
     r := nRow + nDir
     if IsOnBoard(r, nCol) .and. Empty( aGrid[r, nCol] )
         AAdd( aMoves, CoordsToSquare(r, nCol) )
         // Move forward 2
         if nRow == nStartRow
             r := nRow + (nDir * 2)
             if IsOnBoard(r, nCol) .and. Empty( aGrid[r, nCol] )
                 AAdd( aMoves, CoordsToSquare(r, nCol) )
             endif
         endif
     endif
     
     // Captures
     aCapCols := { nCol - 1, nCol + 1 }
     for i := 1 to 2
        c := aCapCols[i]
        r := nRow + nDir
        if IsOnBoard(r, c)
           if !Empty( aGrid[r,c] ) .and. !IsOwnPiece( aGrid[r,c], cColor )
              AAdd( aMoves, CoordsToSquare(r, c) )
           endif
        endif
     next
     
  case cType == "N" // Knight
     aOffs := { {-2,-1}, {-2,1}, {-1,-2}, {-1,2}, {1,-2}, {1,2}, {2,-1}, {2,1} }
     for i := 1 to Len(aOffs)
        r := nRow + aOffs[i][1]
        c := nCol + aOffs[i][2]
        if IsOnBoard(r, c)
           if Empty( aGrid[r,c] ) .or. !IsOwnPiece( aGrid[r,c], cColor )
              AAdd( aMoves, CoordsToSquare(r, c) )
           endif
        endif
     next
     
  case cType == "R" .or. cType == "B" .or. cType == "Q" // Sliders
     aDirs := {}
     if cType == "R" .or. cType == "Q" // Rook dirs
         AAdd( aDirs, {-1, 0} ); AAdd( aDirs, {1, 0} ); AAdd( aDirs, {0, -1} ); AAdd( aDirs, {0, 1} )
     endif
     if cType == "B" .or. cType == "Q" // Bishop dirs
         AAdd( aDirs, {-1, -1} ); AAdd( aDirs, {-1, 1} ); AAdd( aDirs, {1, -1} ); AAdd( aDirs, {1, 1} )
     endif
     
     for i := 1 to Len(aDirs)
         r := nRow + aDirs[i][1]
         c := nCol + aDirs[i][2]
         while IsOnBoard(r, c)
             if Empty( aGrid[r,c] )
                 AAdd( aMoves, CoordsToSquare(r, c) )
             else
                 if !IsOwnPiece( aGrid[r,c], cColor )
                     AAdd( aMoves, CoordsToSquare(r, c) )
                 endif
                 exit // Blocked
             endif
             r += aDirs[i][1]
             c += aDirs[i][2]
         end
     next
     
  case cType == "K" // King
     for r := nRow - 1 to nRow + 1
        for c := nCol - 1 to nCol + 1
           if IsOnBoard(r, c) .and. (r != nRow .or. c != nCol)
              if Empty( aGrid[r,c] ) .or. !IsOwnPiece( aGrid[r,c], cColor )
                 AAdd( aMoves, CoordsToSquare(r, c) )
              endif
           endif
        next
     next
     
   endcase
   

return aMoves

function IsOnBoard( r, c )
   return r >= 1 .and. r <= 8 .and. c >= 1 .and. c <= 8

//----------------------------------------------------------------------------//
// FEN/Grid Helpers

function FENToGrid( cFEN )
   local aGrid[8,8]
   local aParts, cBoard, aRows, cRow
   local i, j, r, c, k, ch, nCount
   

   aParts := hb_ATokens( cFEN, " " )
   cBoard := aParts[1]
   aRows := hb_ATokens( cBoard, "/" )
   

   for r := 1 to 8
      cRow := aRows[r]
      c := 1
      for k := 1 to Len(cRow)
         ch := SubStr( cRow, k, 1 )
         if IsDigit( ch )
            nCount := Val( ch )
            for j := 1 to nCount
               aGrid[r, c] := ""
               c++
            next
         else
            aGrid[r, c] := ch
            c++
         endif
      next
   next
return aGrid

function GridToFEN( aGrid )
   local cFEN := ""
   local r, c, i, nEmpty
   

   for r := 1 to 8
      nEmpty := 0
      for c := 1 to 8
         if Empty( aGrid[r,c] )
            nEmpty++
         else
            if nEmpty > 0
               cFEN += AllTrim(Str(nEmpty))
               nEmpty := 0
            endif
            cFEN += aGrid[r,c]
         endif
      next
      if nEmpty > 0
         cFEN += AllTrim(Str(nEmpty))
      endif
      if r < 8; cFEN += "/"; endif
   next
return cFEN

function SquareToCoords( cSquare )
   local cCol := Left( cSquare, 1 )
   local cRow := Right( cSquare, 1 )
   local nCol := Asc( cCol ) - Asc("a") + 1
   local nRow := 8 - (Val( cRow ) - 1) 
   // Matrix: Row 1 is Rank 8 (Top), Row 8 is Rank 1 (Bottom)
   return { nRow, nCol }

function CoordsToSquare( r, c )
   local cCol := Chr( Asc("a") + c - 1 )
   local cRow := AllTrim(Str( 8 - (r - 1) ))
   return cCol + cRow

//----------------------------------------------------------------------------//
// Rendering

function RenderBoard( cFEN, cSelected, aValidMoves )
   local aGrid := FENToGrid( cFEN )
   local cHtml := '<div class="chess-board">'
   local r, c, cSquare, cClass, cContent, cPiece, cColorClass
   local lIsSelected, lIsValid
   

   for r := 1 to 8
      for c := 1 to 8
         cSquare := CoordsToSquare( r, c )
         cPiece := aGrid[r, c]
         

     // Background color
     if (r + c) % 2 == 0
        cClass := "white-square"
     else
        cClass := "black-square"
     endif
     
     // Selection highlight
     if cSquare == cSelected
        cClass += " selected"
     endif
     
     // Piece
     if !Empty(cPiece)
         cContent := GetPieceSymbol( cPiece )
         cColorClass := iif( IsUpperLocal(cPiece), " piece-white", " piece-black" )
     else
         cContent := ""
         cColorClass := ""
     endif

     // Valid move marker
     lIsValid := .F.
     if ASCAN( aValidMoves, cSquare ) > 0
        cClass += " valid-move"
        lIsValid := .T.
     endif
     
     cHtml += '<div class="square ' + cClass + '" '
     cHtml += 'onclick="selectSquare(' + Chr(39) + cSquare + Chr(39) + ')">'
     
     if !Empty(cContent)
        cHtml += '<span class="piece' + cColorClass + '">' + cContent + '</span>'
     endif
     
     if lIsValid .and. Empty(cContent)
         cHtml += '<div class="move-dot"></div>'
     endif

     cHtml += '</div>'
     
  next
   next
   cHtml += '</div>'
return cHtml

function GetPieceSymbol( cPiece )
   do case
      case Upper(cPiece) == "P"
         return "♟" // User wanted simple PRG, using generic chess glyphs
      case Upper(cPiece) == "R"
         return "♜"
      case Upper(cPiece) == "N"
         return "♞"
      case Upper(cPiece) == "B"
         return "♝"
      case Upper(cPiece) == "Q"
         return "♛"
      case Upper(cPiece) == "K"
         return "♚"
   endcase
return cPiece

//----------------------------------------------------------------------------//
// View Helper (Standard HIX pattern)

function View( cName, hParams )
   local cData
   local cViewPath := "c:\temp\views\" + cName + ".view"
   if hParams == nil; hParams := {=>}; endif
   if File( cViewPath )
      cData = MemoRead( cViewPath )
      while ReplaceBlocks( @cData, "{{", "}}", hParams )
      end
   else
      cData = "<h2>" + cName + " not found!</h2>" 
   endif    

return cData

function ReplaceBlocks( cCode, cStartBlock, cEndBlock, hParams )
   local nStart, nEnd, cBlock, cParamName
   local lReplaced := .F.
   while ( nStart := At( cStartBlock, cCode ) ) != 0 .and. ( nEnd := At( cEndBlock, cCode ) ) != 0
      cBlock = SubStr( cCode, nStart + Len( cStartBlock ), nEnd - nStart - Len( cEndBlock ) )
      cParamName := AllTrim( cBlock )
      if hb_HHasKey( hParams, cParamName )
          cCode = SubStr( cCode, 1, nStart - 1 ) + ValToChar( hParams[ cParamName ] ) + SubStr( cCode, nEnd + Len( cEndBlock ) )
      else
          cCode = SubStr( cCode, 1, nStart - 1 ) + "" + SubStr( cCode, nEnd + Len( cEndBlock ) )
      endif
      lReplaced = .T.
   end
return lReplaced

function ValToChar( u )
   local cType := ValType( u )
   if cType == "C"; return u; endif
   if cType == "N"; return AllTrim(Str(u)); endif
   if cType == "A"; return "{...}"; endif
   if cType == "O"; return "[Object]"; endif
   if cType == "L"; return iif(u, ".T.", ".F."); endif
   if cType == "U"; return "NIL"; endif
   return ""

function IsUpperLocal( cChar )
   local nAsc := Asc( cChar )
   return nAsc >= 65 .and. nAsc <= 90

views/chess.view

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hix Chess</title>
  {{View("style")}}
  <style>
    .chess-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        margin-top: 20px;
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
    }
    

.chess-board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    width: 480px;
    height: 480px;
    border: 10px solid #4a3021;
    user-select: none;
}

.square {
    width: 60px;
    height: 60px;
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
    cursor: default;
}

.white-square {
    background-color: #f0d9b5;
}

.black-square {
    background-color: #b58863;
}

.selected {
    background-color: #f5f682 !important;
}

.valid-move {
    cursor: pointer;
}

.move-dot {
    width: 20px;
    height: 20px;
    background-color: rgba(0, 0, 0, 0.2);
    border-radius: 50%;
    position: absolute;
    pointer-events: none;
}

.square:hover .move-dot {
    background-color: rgba(0, 0, 0, 0.4);
}

/* Piece Styles */
.piece {
    font-size: 40px;
    cursor: pointer;
    z-index: 10;
    /* Prevent text selection */
    -webkit-user-select: none;
    user-select: none;
}

.piece-white {
    color: #fff;
    text-shadow: 1px 1px 2px #000;
}

.piece-black {
    color: #000;
    text-shadow: 0px 0px 1px #fff; /* Slight halo for visibility on dark */
}

.controls {
    margin-top: 20px;
    display: flex;
    gap: 15px;
    align-items: center;
}

.btn {
    padding: 10px 20px;
    background-color: #5d4037;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
}

.btn:hover {
    background-color: #3e2723;
}

.status-panel {
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 10px;
    color: #333;
}

.turn-indicator {
    margin-left: 10px;
    padding: 5px 10px;
    background: #eee;
    border-radius: 4px;
}

  </style>
</head>
<body>
  <div class="chess-container">
    <h1>HIX Chess</h1>
    

<div class="status-panel">
    Turn: <span class="turn-indicator">{{turn}}</span>
</div>

<form id="chessForm" action="chess.prg" method="POST">
    <input type="hidden" name="action" id="action" value="">
    <input type="hidden" name="fen" id="fen" value="{{fen}}">
    <input type="hidden" name="selected" id="selected" value="{{selected}}">
    <input type="hidden" name="clicked_square" id="clicked_square" value="">
    
    <!-- Board injected from PRG -->
    {{board_html}}
    
    <div class="controls">
        <button type="button" class="btn" onclick="newGame()">New Game</button>
    </div>
</form>

  </div>

  <script>
    function selectSquare(squareId) {
        document.getElementById('action').value = 'select';
        document.getElementById('clicked_square').value = squareId;
        document.getElementById('chessForm').submit();
    }

function newGame() {
    document.getElementById('action').value = 'new';
    document.getElementById('chessForm').submit();
}
  </script>
</body>
</html>

views/style.view

<style>
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
    color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .container {
    background: white;
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    width: 350px; /* Fixed width for calculator */
  }

  h1 {
    color: #333;
    margin-bottom: 20px;
    font-size: 24px;
    text-align: right;
    font-weight: 300;
  }

  .calculator-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
  }

  .calc-display {
    grid-column: 1 / -1;
    background-color: #f8f9fa;
    color: #333;
    padding: 20px;
    border-radius: 10px;
    font-size: 2rem;
    text-align: right;
    border: none;
    margin-bottom: 20px;
    box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
  }

  .calc-btn {
    border: none;
    background-color: #f0f0f0;
    font-size: 1.25rem;
    color: #333;
    padding: 20px;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.2s;
    user-select: none;
  }

  .calc-btn:hover {
    background-color: #e0e0e0;
  }

  .calc-btn.operator {
    background-color: #e0e0e0;
    color: #333;
    font-weight: bold;
    font-size: 1.4rem; /* Larger font for operators */
  }
  

  .calc-btn.operator:hover {
    background-color: #d0d0d0;
  }

  .calc-btn.equals {
    grid-column: span 2;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
  }
  

  .calc-btn.equals:hover {
    opacity: 0.9;
  }
  

  .calc-btn.clear {
    background-color: #ffebee;
    color: #d32f2f;
  }
  

  .calc-btn.clear:hover {
    background-color: #ffcdd2;
  }

  /* Hide actual form elements */
  .hidden-form {
    display: none;
  }
  

  /* Result specific */
  .result-box {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
    margin-bottom: 20px;
  }

  .result-value {
    font-size: 36px;
    color: #667eea;
    font-weight: bold;
    margin-top: 10px;
  }
</style>
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 07, 2025 03:48 PM

sudokus.prg

// Sudoku Game Logic

function Main()
   local hPost, cHtml, cAction, hParams
   local cGrid, cSolution, cUserGrid, cMessage, lSolved
   

   hPost := UPost()
   cHtml := ""
   cMessage := ""
   lSolved := .F.

   if hb_HHasKey( hPost, "action" )
      cAction := hPost["action"]
   else
      cAction := "new"
   endif

   do case
      case cAction == "new"
         // Generate new puzzle
         hParams := GeneratePuzzle()
         hParams["message"] := "New Game Started"
         hParams["solved"] := .F.
         

  case cAction == "check"
     // Retrieve state from hidden fields
     cSolution := hPost["solution"]
     cGrid := hPost["initial_grid"] // The puzzle with holes
     cUserGrid := GetUserGrid( hPost ) // Extract user inputs
     
     if CheckSolution( cUserGrid, cSolution )
        cMessage := "Congratulations! You solved it!"
        lSolved := .T.
     else
        cMessage := "Keep trying! Some numbers are incorrect."
        lSolved := .F.
     endif

     hParams := {;
        "grid" => cUserGrid, ;
        "initial_grid" => cGrid, ;
        "solution" => cSolution, ;
        "message" => cMessage, ;
        "solved" => lSolved ;
     }
     
  case cAction == "hint"
     cSolution := hPost["solution"]
     cGrid := hPost["initial_grid"]
     cUserGrid := GetUserGrid( hPost )
     
     cUserGrid := ApplyHint( cUserGrid, cSolution )
     cMessage := "Hint added!"
     
     hParams := {;
        "grid" => cUserGrid, ;
        "initial_grid" => cGrid, ;
        "solution" => cSolution, ;
        "message" => cMessage, ;
        "solved" => .F. ;
     }
   endcase

   cHtml := View( "sudokus", hParams )

return cHtml

//----------------------------------------------------------------------------//

function GeneratePuzzle()
   local aGrid[9,9]
   local aSolution[9,9]
   local i, j, nVal, k
   local hResult
   local cGridStr := ""
   local cSolutionStr := ""

   // 1. Initialize empty
   for i := 1 to 9
      for j := 1 to 9
         aGrid[i,j] := 0
      next
   next

   // 2. Fill diagonal 3x3 boxes (independent) to ensure validity and randomness
   FillDiagonal( aGrid )

   // 3. Solve the rest to create a full valid solution
   SolveSudoku( aGrid )
   

   // Store solution
   for i := 1 to 9
      for j := 1 to 9
         aSolution[i,j] := aGrid[i,j]
         cSolutionStr += AllTrim(Str(aGrid[i,j])) + ","
      next
   next
   

   // 4. Remove digits to create the puzzle
   RemoveDigits( aGrid, 40 ) // Remove 40 digits

   // Create grid string for display/storage
   for i := 1 to 9
      for j := 1 to 9
         cGridStr += AllTrim(Str(aGrid[i,j])) + ","
      next
   next

   // Remove trailing commas
   if Right(cSolutionStr, 1) == ","
      cSolutionStr := Left(cSolutionStr, Len(cSolutionStr)-1)
   endif
   if Right(cGridStr, 1) == ","
      cGridStr := Left(cGridStr, Len(cGridStr)-1)
   endif

   hResult := {;
      "grid" => cGridStr, ;
      "initial_grid" => cGridStr, ;
      "solution" => cSolutionStr ;
   }

return hResult

//----------------------------------------------------------------------------//

function FillDiagonal( aGrid )
   local k
   for k := 0 to 2
      FillBox( aGrid, k*3 + 1, k*3 + 1 )
   next
return nil

function FillBox( aGrid, nRow, nCol )
   local nNum, i, j
   local x, y
   local nTemp, nRand
   local aNums := {1,2,3,4,5,6,7,8,9} 
   local nIdx := 1 
   

   // Simple random fill for box (naive, better to shuffle)
   // Implementing Fisher-Yates shuffle for aNums would be better
   // But for Harbour, let's just picking random unused.
  

   // Shuffle aNums
   for i := 9 to 2 step -1
       nRand := hb_RandomInt( 1, i )
       nTemp := aNums[i]
       aNums[i] := aNums[nRand]
       aNums[nRand] := nTemp
   next
   

   nIdx := 1
   for i := 0 to 2
      for j := 0 to 2
         aGrid[nRow + i, nCol + j] := aNums[nIdx]
         nIdx++
      next
   next
return nil

//----------------------------------------------------------------------------//

function SolveSudoku( aGrid )
   local nRow, nCol
   local aUnassigned := FindUnassignedLocation( aGrid )
   local nNum
   

   if aUnassigned[1] == -1
      return .T. // Success
   endif
   

   nRow := aUnassigned[1]
   nCol := aUnassigned[2]
   

   for nNum := 1 to 9
      if IsSafe( aGrid, nRow, nCol, nNum )
         aGrid[nRow, nCol] := nNum
         

     if SolveSudoku( aGrid )
        return .T.
     endif
     
     aGrid[nRow, nCol] := 0
  endif
   next

return .F.

//----------------------------------------------------------------------------//

function FindUnassignedLocation( aGrid )
   local i, j
   for i := 1 to 9
      for j := 1 to 9
         if aGrid[i,j] == 0
            return {i, j}
         endif
      next
   next
return {-1, -1}

//----------------------------------------------------------------------------//

function IsSafe( aGrid, nRow, nCol, nNum )
   return !UsedInRow( aGrid, nRow, nNum ) .and. ;
          !UsedInCol( aGrid, nCol, nNum ) .and. ;
          !UsedInBox( aGrid, nRow - (nRow-1)%3, nCol - (nCol-1)%3, nNum )

function UsedInRow( aGrid, nRow, nNum )
   local c
   for c := 1 to 9
      if aGrid[nRow, c] == nNum; return .T.; endif
   next
return .F.

function UsedInCol( aGrid, nCol, nNum )
   local r
   for r := 1 to 9
      if aGrid[r, nCol] == nNum; return .T.; endif
   next
return .F.

function UsedInBox( aGrid, nBoxStartRow, nBoxStartCol, nNum )
   local r, c
   for r := 0 to 2
      for c := 0 to 2
         if aGrid[nBoxStartRow + r, nBoxStartCol + c] == nNum; return .T.; endif
      next
   next
return .F.

//----------------------------------------------------------------------------//

function RemoveDigits( aGrid, nCount )
   local r, c
   local i := 0
   

   while i < nCount
      r := hb_RandomInt( 1, 9 )
      c := hb_RandomInt( 1, 9 )
      if aGrid[r,c] != 0
         aGrid[r,c] := 0
         i++
      endif
   end
return nil

//----------------------------------------------------------------------------//

function GetUserGrid( hPost )
   local i, j, cKey, cVal
   local cUserGrid := ""
   

   for i := 1 to 9
      for j := 1 to 9
         cKey := "cell_" + AllTrim(Str(i)) + "_" + AllTrim(Str(j))
         if hb_HHasKey( hPost, cKey )
             cVal := hPost[cKey]
             if Empty(cVal); cVal := "0"; endif
             cUserGrid += cVal + ","
         else
             cUserGrid += "0,"
         endif
      next
   next
   

   if Right(cUserGrid, 1) == ","
      cUserGrid := Left(cUserGrid, Len(cUserGrid)-1)
   endif
   

return cUserGrid

//----------------------------------------------------------------------------//

function CheckSolution( cUserGrid, cSolution )
   // Simple string comparison for now
   return cUserGrid == cSolution

//----------------------------------------------------------------------------//
// Helper View function (duplicated from calculator.prg for independence)
// Logic for view rendering should ideally be in a shared lib, but following calculator pattern.

function View( cName, hParams )
   local cData
   local cViewPath := "c:\temp\views\" + cName + ".view"
   if hParams == nil; hParams := {=>}; endif

   if File( cViewPath )
      cData = MemoRead( cViewPath )
      while ReplaceBlocks( @cData, "{{", "}}", hParams )
      end
   else
      cData = "<h2>" + cName + " not found!</h2>" 
   endif    

return cData

function ReplaceBlocks( cCode, cStartBlock, cEndBlock, hParams )
   local nStart, nEnd, cBlock, cParamName
   local lReplaced := .F.
   

   while ( nStart := At( cStartBlock, cCode ) ) != 0 .and. ( nEnd := At( cEndBlock, cCode ) ) != 0
      cBlock = SubStr( cCode, nStart + Len( cStartBlock ), nEnd - nStart - Len( cEndBlock ) )
      

  if "View" $ cBlock
     // Simplified nested view handling
      cCode = SubStr( cCode, 1, nStart - 1 ) + View("style") + SubStr( cCode, nEnd + Len( cEndBlock ) ) // Hacky for "style"
  else
     cParamName := AllTrim( cBlock )
     if hb_HHasKey( hParams, cParamName )
         cCode = SubStr( cCode, 1, nStart - 1 ) + ValToChar( hParams[ cParamName ] ) + SubStr( cCode, nEnd + Len( cEndBlock ) )
     else
         cCode = SubStr( cCode, 1, nStart - 1 ) + "" + SubStr( cCode, nEnd + Len( cEndBlock ) )
     endif
  endif
  lReplaced = .T.
   end
return lReplaced

function ValToChar( u )
   if ValType(u) == "C"; return u; endif
   if ValType(u) == "N"; return AllTrim(Str(u)); endif
   if ValType(u) == "L"; return IIF(u, "true", "false"); endif
return ""

//----------------------------------------------------------------------------//

function ApplyHint( cUserGrid, cSolution )
   local aUser := hb_ATokens( cUserGrid, "," )
   local aSol := hb_ATokens( cSolution, "," )
   local i
   local aEmpty := {}
   local nPick, cNewVal
   

   // Find empty or incorrect cells
   for i := 1 to Len(aUser)
      if Val(aUser[i]) == 0
         AAdd( aEmpty, i )
      endif
   next
   

   if Len(aEmpty) > 0
      // Pick random empty spot
      nPick := hb_RandomInt( 1, Len(aEmpty) )
      i := aEmpty[nPick]
      

  // Update Grid (string manipulation would be harder, array is easier)
  aUser[i] := aSol[i]
  
  // Rebuild string
  cUserGrid := ""
  for i := 1 to Len(aUser)
     cUserGrid += aUser[i] + ","
  next
  if Right(cUserGrid, 1) == ","
     cUserGrid := Left(cUserGrid, Len(cUserGrid)-1)
  endif
   endif

return cUserGrid

views/sudokus.view

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hix Sudoku</title>
  {{View("style")}}
  <style>
    .sudoku-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        margin-top: 20px;
    }
    .sudoku-grid {
        display: grid;
        grid-template-columns: repeat(9, 1fr);
        gap: 0;
        border: 2px solid #333;
        width: 360px; /* 40px per cell */
        height: 360px;
    }
    .sudoku-cell {
        width: 40px;
        height: 40px;
        text-align: center;
        font-size: 20px;
        border: 1px solid #ddd;
        box-sizing: border-box;
        padding: 0;
        margin: 0;
    }
    /* Thicker borders for 3x3 boxes */
    .sudoku-cell:nth-child(3n) {
        border-right: 2px solid #333;
    }
    .sudoku-cell:nth-child(9n) {
        border-right: 1px solid #ddd; /* Reset for last col */
    }
    /* Right border for edge of grid handled by container */
    

/* Rows */
.sudoku-cell.box-bottom {
    border-bottom: 2px solid #333;
}

input.sudoku-cell {
    outline: none;
    color: #333;
    background-color: white;
}
input.sudoku-cell:focus {
    background-color: #e0f7fa;
}
input.readonly {
    background-color: #f5f5f5;
    color: #555;
    font-weight: bold;
    cursor: default;
}

.controls {
    margin-top: 20px;
    display: flex;
    gap: 10px;
}
.btn {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
}
.btn:hover {
    background-color: #0056b3;
}
.btn-new {
    background-color: #28a745;
}
.btn-new:hover {
    background-color: #218838;
}
.message {
    margin-bottom: 15px;
    font-size: 18px;
    font-weight: bold;
    color: #333;
    height: 24px;
}
.success { color: green; }
.error { color: red; }
  </style>
</head>
<body>
  <div class="container sudoku-container">
    <h1>HIX Sudoku</h1>
    

<div id="messageArea" class="message">{{message}}</div>

<form id="sudokuForm" action="sudokus.prg" method="POST">
    <input type="hidden" name="action" id="action" value="check">
    <input type="hidden" name="solution" value="{{solution}}">
    <input type="hidden" name="initial_grid" value="{{initial_grid}}">
    
    <div class="sudoku-grid" id="grid">
        <!-- Grid generated by JS -->
    </div>

    <div class="controls">
        <button type="button" class="btn" onclick="submitCheck()">Check Solution</button>
        <button type="button" class="btn" style="background-color: #ffc107; color: #000;" onclick="getHint()">Hint</button>
        <button type="button" class="btn btn-new" onclick="newGame()">New Game</button>
    </div>
</form>

  </div>

  <script>
    // Data passed from server
    // Formats: "1,0,5,..."
    const currentGridStr = "{{grid}}"; 
    const initialGridStr = "{{initial_grid}}";
    

const currentGrid = currentGridStr.split(',');
const initialGrid = initialGridStr.split(',');

const gridContainer = document.getElementById('grid');

function renderGrid() {
    gridContainer.innerHTML = '';
    
    for (let i = 0; i < 81; i++) {
        const row = Math.floor(i / 9) + 1;
        const col = (i % 9) + 1;
        const val = currentGrid[i];
        const isInitial = initialGrid[i] !== '0';
        
        const input = document.createElement('input');
        input.type = 'text';
        input.className = 'sudoku-cell';
        input.maxLength = 1;
        input.name = 'cell_' + row + '_' + col;
        input.id = 'cell_' + i;
        input.value = val === '0' ? '' : val;
        
        // 3x3 Box Styling
        if (col % 3 === 0 && col !== 9) {
            input.style.borderRight = "2px solid #333";
        }
        if (row % 3 === 0 && row !== 9) {
            input.style.borderBottom = "2px solid #333";
        }

        if (isInitial) {
            input.readOnly = true;
            input.classList.add('readonly');
        } else {
            input.addEventListener('input', (e) => {
                const v = e.target.value;
                if (!/^[1-9]$/.test(v)) {
                    e.target.value = '';
                }
            });
            
            // Navigation
            input.addEventListener('keydown', (e) => handleKey(e, i));
        }
        
        gridContainer.appendChild(input);
    }
}

function handleKey(e, index) {
    let nextIndex = index;
    if (e.key === 'ArrowRight') nextIndex++;
    if (e.key === 'ArrowLeft') nextIndex--;
    if (e.key === 'ArrowUp') nextIndex -= 9;
    if (e.key === 'ArrowDown') nextIndex += 9;
    
    if (nextIndex >= 0 && nextIndex < 81 && nextIndex !== index) {
        e.preventDefault();
        const el = document.getElementById('cell_' + nextIndex);
        if (el) el.focus();
    }
}

function submitCheck() {
    // Fill empty inputs with 0 before submitting to keep grid structure aligned if necessary
    // actually main logic uses cell_x_y keys so we are fine.
    document.getElementById('action').value = 'check';
    document.getElementById('sudokuForm').submit();
}

function getHint() {
     document.getElementById('action').value = 'hint';
     document.getElementById('sudokuForm').submit();
}

function newGame() {
    document.getElementById('action').value = 'new';
    document.getElementById('sudokuForm').submit();
}

// Initialize
if (currentGrid.length === 81) {
    renderGrid();
} else {
    // Fallback or loading state
    gridContainer.innerHTML = '<p>Loading Grid...</p>';
}

// Style adjustments for messages
const msg = document.getElementById('messageArea');
if (msg.textContent.includes('Congratulations')) {
    msg.classList.add('success');
} else if (msg.textContent.includes('try')) {
    msg.classList.add('error');
}

  </script>
</body>
</html>

views/style.view

<style>
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
    color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .container {
    background: white;
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    width: 350px; /* Fixed width for calculator */
  }

  h1 {
    color: #333;
    margin-bottom: 20px;
    font-size: 24px;
    text-align: right;
    font-weight: 300;
  }

  .calculator-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
  }

  .calc-display {
    grid-column: 1 / -1;
    background-color: #f8f9fa;
    color: #333;
    padding: 20px;
    border-radius: 10px;
    font-size: 2rem;
    text-align: right;
    border: none;
    margin-bottom: 20px;
    box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
  }

  .calc-btn {
    border: none;
    background-color: #f0f0f0;
    font-size: 1.25rem;
    color: #333;
    padding: 20px;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.2s;
    user-select: none;
  }

  .calc-btn:hover {
    background-color: #e0e0e0;
  }

  .calc-btn.operator {
    background-color: #e0e0e0;
    color: #333;
    font-weight: bold;
    font-size: 1.4rem; /* Larger font for operators */
  }
  

  .calc-btn.operator:hover {
    background-color: #d0d0d0;
  }

  .calc-btn.equals {
    grid-column: span 2;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
  }
  

  .calc-btn.equals:hover {
    opacity: 0.9;
  }
  

  .calc-btn.clear {
    background-color: #ffebee;
    color: #d32f2f;
  }
  

  .calc-btn.clear:hover {
    background-color: #ffcdd2;
  }

  /* Hide actual form elements */
  .hidden-form {
    display: none;
  }
  

  /* Result specific */
  .result-box {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
    margin-bottom: 20px;
  }

  .result-value {
    font-size: 36px;
    color: #667eea;
    font-weight: bold;
    margin-top: 10px;
  }
</style>
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 1445
Joined: Mon Oct 10, 2005 02:38 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 07, 2025 05:58 PM

Antonio...

Una cosa...

Entre una imagen y la otra han transcurrido 27 minutos.

Esto es lo que has tardado en 'hacer' la aplicación de Sudoku?

Un Saludo

Carlos G.



FiveWin 25.12 + Harbour 3.2.0dev (r2502110321), BCC 7.7 Windows 11 Home

Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 07, 2025 11:14 PM

Increible a la velocidad que construye AntiGravity :!: :wink:

regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 08, 2025 03:08 AM

calculator.prg

// Calculator App logic

function Main()

   local hPost := UPost()
   local cHtml := ""
   local nNum1, nNum2, cOp, nResult, hParams

   if hb_HHasKey( hPost, "num1" )
      

  nNum1 := Val( hPost[ "num1" ] )
  nNum2 := Val( hPost[ "num2" ] )
  cOp   := hPost[ "op" ]
  
  do case
     case cOp == "+"
          nResult := nNum1 + nNum2
     case cOp == "-"
          nResult := nNum1 - nNum2
     case cOp == "*"
          nResult := nNum1 * nNum2
     case cOp == "/"
          if nNum2 != 0
             nResult := nNum1 / nNum2
          else
             nResult := "Error: Div by Zero"
          endif
     case cOp == "^"
          nResult := nNum1 ^ nNum2
     case cOp == "%"
          nResult := nNum1 % nNum2
  endcase

  hParams := { "cResult" => nResult, "nNum1" => nNum1, "nNum2" => nNum2, "cOp" => cOp }
  cHtml := View( "calculator", hParams )

   else
      hParams := { "cResult" => "0" }
      cHtml := View( "calculator", hParams ) 
   endif

return cHtml

//----------------------------------------------------------------------------//

function View( cName, hParams )

   local cData
   local cViewPath := "c:\temp\views\" + cName + ".view"

   if hParams == nil
      hParams := {=>}
   endif

   if File( cViewPath )
      cData = MemoRead( cViewPath )
      while ReplaceBlocks( @cData, "{{", "}}", hParams )
      end
   else
      cData = "<h2>" + cName + " not found!</h2>" 
   endif    

return cData

//----------------------------------------------------------------------------//

function ReplaceBlocks( cCode, cStartBlock, cEndBlock, hParams )

   local nStart, nEnd, cBlock, nQ1, nQ2
   local lReplaced := .F.
   local cParamName
   

   while ( nStart := At( cStartBlock, cCode ) ) != 0 .and. ;
         ( nEnd := At( cEndBlock, cCode ) ) != 0
      cBlock = SubStr( cCode, nStart + Len( cStartBlock ), nEnd - nStart - Len( cEndBlock ) )
      

  // Keep it simple for this example, only support variable replacement or View call
  if "View" $ cBlock
     // Manual parsing to avoid macro compiler issues with function visibility: View("name")
     nQ1 := At( '"', cBlock )
     nQ2 := RAt( '"', cBlock )
     
     if nQ1 > 0 .and. nQ2 > nQ1
        cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                View( SubStr( cBlock, nQ1 + 1, nQ2 - nQ1 - 1 ) ) + ;
                SubStr( cCode, nEnd + Len( cEndBlock ) )
     else
        cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                "" + ;
                SubStr( cCode, nEnd + Len( cEndBlock ) )
     endif
  else
     // Simple variable replacement
     cParamName := AllTrim( cBlock )
     if hb_HHasKey( hParams, cParamName )
         cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                 ValToChar( hParams[ cParamName ] ) + ;
                 SubStr( cCode, nEnd + Len( cEndBlock ) )
     else
         // Unknown block, just remove brackets
         cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                 "" + ;
                 SubStr( cCode, nEnd + Len( cEndBlock ) )
     endif
  endif

  lReplaced = .T.
   end
   

return lReplaced

//----------------------------------------------------------------------------//

function ValToChar( u )
   local cType := ValType( u )
   local cResult

   do case
      case cType == "C"
           cResult = u
      case cType == "N"
           cResult = AllTrim( Str( u ) )
      otherwise
           cResult = ""
   endcase
return cResult

views/calculator.view

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hix Calculator</title>
  {{View("style")}}
</head>
<body>
  <div class="container">
    <h1>HIX Calculator</h1>
    

<div class="calculator-grid">
    <div id="display" class="calc-display">0</div>
    
    <button class="calc-btn clear" onclick="clearCalc()">C</button>
    <button class="calc-btn operator" onclick="setOp('/')">&divide;</button>
    <button class="calc-btn operator" onclick="setOp('*')">&times;</button>
    <button class="calc-btn operator" onclick="setOp('-')">-</button>
    
    <button class="calc-btn" onclick="appendNum(7)">7</button>
    <button class="calc-btn" onclick="appendNum(8)">8</button>
    <button class="calc-btn" onclick="appendNum(9)">9</button>
    <button class="calc-btn operator" onclick="setOp('+')">+</button>
    
    <button class="calc-btn" onclick="appendNum(4)">4</button>
    <button class="calc-btn" onclick="appendNum(5)">5</button>
    <button class="calc-btn" onclick="appendNum(6)">6</button>
    <button class="calc-btn operator" onclick="setOp('^')">x<sup>y</sup></button>
    
    <button class="calc-btn" onclick="appendNum(1)">1</button>
    <button class="calc-btn" onclick="appendNum(2)">2</button>
    <button class="calc-btn" onclick="appendNum(3)">3</button>
    <button class="calc-btn operator" onclick="setOp('%')">%</button>

    <button class="calc-btn" onclick="appendNum(0)">0</button>
    <button class="calc-btn" onclick="appendNum('.')">.</button>
    <button class="calc-btn equals" onclick="calculate()" style="grid-column: span 2;">=</button>
</div>

<form id="calcForm" action="calculator.prg" method="POST" class="hidden-form">
  <input type="hidden" name="num1" id="num1">
  <input type="hidden" name="op" id="op">
  <input type="hidden" name="num2" id="num2">
</form>
  </div>

  <script>
    let serverResult = '{{cResult}}';
    let currentInput = '';
    let firstNum = '';
    let operator = '';
    let isNewEntry = true;
    

// Initialize display with server result if available
const displayElement = document.getElementById('display');
if (serverResult !== '' && serverResult !== '0') {
     currentInput = serverResult;
     isNewEntry = true; 
     // Note: If user presses operator next, we want firstNum to be this result.
     // But my SetOp logic uses currentInput if firstNum is empty.
     // So setting currentInput is correct. 
}
updateDisplay(serverResult || '0');

function updateDisplay(value) {
    displayElement.textContent = value || '0';
}

function appendNum(num) {
    if (isNewEntry) {
        currentInput = '';
        isNewEntry = false;
    }
    if (num === '.' && currentInput.includes('.')) return;
    currentInput += num;
    updateDisplay(currentInput);
}

function setOp(op) {
    if (currentInput === '' && firstNum === '') return;
    
    if (firstNum !== '' && !isNewEntry) {
        // Chaining operations? For simple version, just replacing op
        // or we could calculate intermediate result here. 
        // Let's keep it simple: set first number to current
    }
    
    firstNum = currentInput;
    operator = op;
    isNewEntry = true;
    
    // Visual feedback could go here
    updateDisplay(firstNum); // Keep showing first number
}

function clearCalc() {
    currentInput = '';
    firstNum = '';
    operator = '';
    isNewEntry = true;
    updateDisplay('0');
}

function calculate() {
    if (firstNum === '' || operator === '' || currentInput === '') return;

    document.getElementById('num1').value = firstNum;
    document.getElementById('op').value = operator;
    document.getElementById('num2').value = currentInput;
    
    document.getElementById('calcForm').submit();
}
  </script>
</body>
</html>

views/style.view

<style>
  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
    color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .container {
    background: white;
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    width: 350px; /* Fixed width for calculator */
  }

  h1 {
    color: #333;
    margin-bottom: 20px;
    font-size: 24px;
    text-align: right;
    font-weight: 300;
  }

  .calculator-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
  }

  .calc-display {
    grid-column: 1 / -1;
    background-color: #f8f9fa;
    color: #333;
    padding: 20px;
    border-radius: 10px;
    font-size: 2rem;
    text-align: right;
    border: none;
    margin-bottom: 20px;
    box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
  }

  .calc-btn {
    border: none;
    background-color: #f0f0f0;
    font-size: 1.25rem;
    color: #333;
    padding: 20px;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.2s;
    user-select: none;
  }

  .calc-btn:hover {
    background-color: #e0e0e0;
  }

  .calc-btn.operator {
    background-color: #e0e0e0;
    color: #333;
    font-weight: bold;
    font-size: 1.4rem; /* Larger font for operators */
  }
  

  .calc-btn.operator:hover {
    background-color: #d0d0d0;
  }

  .calc-btn.equals {
    grid-column: span 2;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
  }
  

  .calc-btn.equals:hover {
    opacity: 0.9;
  }
  

  .calc-btn.clear {
    background-color: #ffebee;
    color: #d32f2f;
  }
  

  .calc-btn.clear:hover {
    background-color: #ffcdd2;
  }

  /* Hide actual form elements */
  .hidden-form {
    display: none;
  }
  

  /* Result specific */
  .result-box {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
    margin-bottom: 20px;
  }

  .result-value {
    font-size: 36px;
    color: #667eea;
    font-weight: bold;
    margin-top: 10px;
  }
</style>
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 08, 2025 04:13 AM

modpro.prg

#ifdef __HARBOUR__
   #ifndef __XHARBOUR__
   #xcommand TRY  => BEGIN SEQUENCE WITH {| oErr | Break( oErr ) }
   #xcommand CATCH [<!oErr!>] => RECOVER [USING <oErr>] <-oErr->
   #xcommand FINALLY => ALWAYS
   #xtranslate Throw( <oErr> ) => ( Eval( ErrorBlock(), <oErr> ), Break( <oErr> ) )
   #endif
#endif

//----------------------------------------------------------------------------//

//----------------------------------------------------------------------------//

function Main()

   local cArgs := hb_GetEnv( "QUERY_STRING" )

return View( "default" )      

//----------------------------------------------------------------------------//

function View( cName )

   local cData

   if File( "./views/" + cName + ".view" )
      cData = MemoRead( "./views/" + cName + ".view" )
      while ReplaceBlocks( @cData, "{{", "}}" )
      end
   else
      cData = "<h2>" + cName + " not found!</h2>" 
   endif    

return cData

//----------------------------------------------------------------------------//

function GetCode()

   local cCode 

   cCode := "function Main()" + hb_eol() + ;
            "" + hb_eol() + ;
            "   ? 'Hello world'" + hb_eol() + ;
            "" + hb_eol() + ;
            "return nil"
   

return cCode  

//----------------------------------------------------------------------------//

function ReplaceBlocks( cCode, cStartBlock, cEndBlock, cParams, ... )

   local nStart, nEnd, cBlock, nQ1, nQ2
   local lReplaced := .F.
   

   hb_default( @cStartBlock, "{{" )
   hb_default( @cEndBlock, "}}" )
   hb_default( @cParams, "" )

   while ( nStart := At( cStartBlock, cCode ) ) != 0 .and. ;
         ( nEnd := At( cEndBlock, cCode ) ) != 0
      cBlock = SubStr( cCode, nStart + Len( cStartBlock ), nEnd - nStart - Len( cEndBlock ) )
      

  // Keep it simple for this example, only support variable replacement or View call
  if "View" $ cBlock
     // Manual parsing to avoid macro compiler issues with function visibility: View("name")
     nQ1 := At( '"', cBlock )
     nQ2 := RAt( '"', cBlock )
     
     if nQ1 > 0 .and. nQ2 > nQ1
        cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                View( SubStr( cBlock, nQ1 + 1, nQ2 - nQ1 - 1 ) ) + ;
                SubStr( cCode, nEnd + Len( cEndBlock ) )
     else
        cCode = SubStr( cCode, 1, nStart - 1 ) + ;
                "" + ;
                SubStr( cCode, nEnd + Len( cEndBlock ) )
     endif
  elseif "GetCode" $ cBlock
      cCode = SubStr( cCode, 1, nStart - 1 ) + ;
              GetCode() + ;
              SubStr( cCode, nEnd + Len( cEndBlock ) )
  else
    // Fallback for ModPro's original Eval use-case
      TRY
         cCode = SubStr( cCode, 1, nStart - 1 ) + ;
              ValToChar( Eval( &( "{ |" + cParams + "| " + cBlock + " }" ), ... ) ) + ;
              SubStr( cCode, nEnd + Len( cEndBlock ) )
      CATCH
         cCode = SubStr( cCode, 1, nStart - 1 ) + ;
              "Error evaluating: " + cBlock + ;
              SubStr( cCode, nEnd + Len( cEndBlock ) )
      END
  endif
      lReplaced = .T.
   end
   

return If( HB_PIsByRef( 1 ), lReplaced, cCode )

function ValToChar( u )

   local cType := ValType( u )
   local cResult

   do case
      case cType == "C" .or. cType == "M"
           cResult = u

  case cType == "D"
       cResult = DToC( u )

  case cType == "L"
       cResult = If( u, ".T.", ".F." )

  case cType == "N"
       cResult = AllTrim( Str( u ) )

  case cType == "A"
       cResult = hb_ValToExp( u )

  case cType == "O"
       cResult = ObjToChar( u )

  case cType == "P"
       cResult = "(P)" 

  case cType == "S"
       cResult = "(Symbol)" 
 
  case cType == "H"
       cResult = StrTran( StrTran( hb_JsonEncode( u, .T. ), CRLF, "<br>" ), " ", "&nbsp;" )
       if Left( cResult, 2 ) == "{}"
          cResult = StrTran( cResult, "{}", "{=>}" )
       endif   

  case cType == "U"
       cResult = "nil"

  otherwise
       cResult = "type not supported yet in function ValToChar()"
   endcase

return cResult   

function ObjToChar( o )

   local hObj := {=>}, aDatas := __objGetMsgList( o, .T. )
   local hPairs := {=>}, aParents := __ClsGetAncestors( o:ClassH )

   AEval( aParents, { | h, n | aParents[ n ] := __ClassName( h ) } ) 

   hObj[ "CLASS" ] = o:ClassName()
   hObj[ "FROM" ]  = aParents 

   AEval( aDatas, { | cData | ObjSetData( o, cData, hPairs ) } )
   hObj[ "DATAs" ]   = hPairs
   hObj[ "METHODs" ] = __objGetMsgList( o, .F. )

return ValToChar( hObj )

function ObjSetData( o, cData, hPairs )

   TRY
      hPairs[ cData ] := __ObjSendMsg( o, cData )
   CATCH      

      hPairs[ cData ] := "** protected **"
   END
   

return nil

views/default.view

{{View("head")}}

{{View("body")}}

views/head.view

<html>

<head>
  <title>modpro</title>
   <link rel='stylesheet' href='https://use.fontawesome.com/releases/v5.7.0/css/all.css'>
   <script src="https://fivetechsoft.github.io/xcloud/source/js/xcloud.js"></script>
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
   <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
   <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
   <link rel="stylesheet" type="text/css" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/themes/base/jquery-ui.css"/>
   <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

   <style>
      .dropdown-menu{
         color:white;
      }
      .dropdown-item:hover{
         background-color: gray;
         color:black;
      }
      .navbar-nav{
         padding-left:15px;
      }
      .navbar-nav:hover{
         background-color: gray;
      }
      .nav-link.dropdown-toggle{
         padding-left:15px;
      }
      .dropdown-toggle::after{
         border-top:0px;
      }
      .dropdown-menu.dropdown-menu-left.bg-dark.show,
      .dropdown.open .dropdown-menu.dropdown-menu-left.bg-dark {
         margin-top:-8px;
         margin-left:-16px;
         box-shadow: 5px 5px 5px #606060;
      }
      .navbar.navbar-expand-lg.navbar-light.bg-light.navbar-dark.bg-dark{
         margin-top:-5px;
         margin-bottom:-5px;
         padding-top:0px;
         padding-bottom:0px;
      }
      .modal-backdrop{
        display:none;
      }
      .modal-dialog{
         display:none;
      }

  #body{
     background-color:aquamarine;
     height: calc( 100% - 25px );
     width: 100%;
     position: relative;
  }
  
  #editor {
    margin-left: 0px;
    width:946px;
    float: left;
    bottom: 0px;
    position: absolute;
    top: 0px;
    white-space: pre;
  }

  #splitter{
     width:5px;
     background-color: white;
     cursor:col-resize;
     float:left;
     height: 100%;
  }
  
  #right{
      font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
      margin-left: 947px;
      position: absolute;
      top: 0px;
      bottom: 0px;
  }
  
  #output{
    height: 100%;
    padding: 10px;
    width: 100%;
    min-width: 50px;
  }   

  .fa-1x {
     font-size: 0.85em;
  }
   </style>

   <script>
      function Run()
      {
          var o = new Object();
      

      o[ 'source' ] = editor.getValue();
      o[ 'params' ] = sessionStorage.getItem( 'params' ); // Also send params if any
            
      $.post( "run.prg", o )
        .done( function( data ) { $('#output').html( data ); } )
        .fail( function( data ) { console.log( 'ERROR', data ); } ); 
  }

  function Save()
  {
      var o = new Object();
  
      o[ 'source' ] = editor.getValue();
      console.log( 'PARAM', o );
            
      $.post( "id.prg", o )
        .done( function( data ) { console.log( 'DONE', data ); location.href="id.prg?" + data; } )
        .fail( function( data ) { console.log( 'ERROR', data ); } ); 
  }

  var nWnd = 1;
  var oForm;

  function Command( cMsg, lScroll )
  {  
     var cId = "command" + nWnd++;

     if( lScroll == null )
        lScroll = false;

     $( '<div class="form" id="' + cId + 
        '" style="overflow:' + ( lScroll ? 'auto': 'none' ) + 
        ';position: absolute;width: 700px;height: 400px;' + 
        'top: 375px;left: 1016px;background-color:silver;box-shadow:5px 5px 5px 0, 0px 0px 5px 0;' + 
        'border-radius:5px;"></div>' ).appendTo( "#main" );
     
     $( "#" + cId ).draggable().resizable().click(function(){
        if( $(this).is('.ui-draggable-dragging') ) {
              return;
        }
        $(".form").css( "z-index", 1 );
        $(this).css( "z-index", nWnd - 1 );
     });

     if( cMsg != null )
        $( "#" + cId ).html( cMsg );
   
     oForm = $( "#" + cId );

     return $( "#" + cId );
  }

  function CloseAllForms()
  {
     nWnd = 1; 
     $(".form").remove();
  }

  var oFPicker;

  function OpenPrg()
  { 
     var oFile; 
     oFPicker = document.createElement( "input" );
     oFPicker.type = "file";
     oFPicker.accept = ".prg,.ch,.txt,.h,.c";
     oFPicker.style.visibility = "hidden";
     oFPicker.onchange = function( evt ){ 
       var reader = new FileReader();
       oFile = evt.target.files[0];
       reader.onload = function( e ) {
          editor.session.setValue( e.target.result ); 
          sessionStorage.setItem( 'modpro_code', e.target.result ); };
       reader.readAsText( oFile ); 
       }

    oFPicker.click(); 
  } 

  function ClosePrg()
  {
     editor.session.setValue( '' );   
     sessionStorage.setItem( 'modpro_code', '' );
  }

  function OpenDbf()
  { 
    var oFile; 
    oFPicker = document.createElement( "input" );
    oFPicker.type = "file";
    oFPicker.accept = ".dbf,.dbt,.fpt,.cdx,.ntx";
    oFPicker.style.visibility = "hidden";
    oFPicker.onchange = function( evt ){ 
       var reader = new FileReader();
       oFile = evt.target.files[0];
       reader.readAsDataURL( oFile ); 
       reader.onload = function( e ) {
           var formData = new FormData();
           var xhr = new XMLHttpRequest();
           var blob = new Blob( [e.target.result], {type: "application/octet-stream"} );
           formData.append( oFile.name, blob );
           xhr.onreadystatechange = function() { 
              if( this.readyState == XMLHttpRequest.DONE && this.status == 200 ) {
                 Command( this.responseText, true ); } };
        xhr.open( "POST", 'upload.prg' );
        xhr.send( formData );
       }
     };

    oFPicker.click(); 
  } 

  function AddLabel()
  {
     var cId = "Label" + ( oForm["0"].children.length - 2 );
     
     oForm.append( "<label class='label control' id='" + cId + "'>" + cId + "</label>" );
     $( "#" + cId ).draggable().resizable();
  }

  function AddInput()
  {
     var cId = "input" + ( oForm["0"].children.length - 2 );

     oForm.append( "<input type='text' class='input control' id='" + cId + "' style='width:300px;height:50px;'></input>" );
     $( "#" + cId ).resizable();
     $( ".ui-wrapper" ).draggable( {cancel:false} );      
  }

  function AddButton()
  {
     var cId = "Button" + ( oForm["0"].children.length - 2 );

     oForm.append( "<button class='button control' id='" + cId + "' style='width:120px;height:60px;'>" + cId + "</button>" );
     $( "#" + cId ).resizable();
     $( ".ui-wrapper" ).draggable( {cancel:false} );
  }

  function ShowHtml()
  {
     var form = {}; 

     form.top    = oForm["0"].style.top;
     form.left   = oForm["0"].style.left;
     form.width  = oForm["0"].style.width;
     form.height = oForm["0"].style.height;
     form.backgroundColor = oForm["0"].style.backgroundColor;
     form.controls = []; 

     $( ".control" ).each( function( n ) {
        form.controls.push( CtrlObj( $( this ) ) ); } );
     
     alert( JSON.stringify( form ) );
  }

  function CtrlObj( oCtrl )
  {
     var control = {};

     control.type   = oCtrl.attr( 'id' );
     control.top    = oCtrl["0"].style.top== 0 ? oCtrl.parent().css( "top" ): oCtrl.css( "top" );
     control.left   = oCtrl["0"].style.left == 0 ? oCtrl.parent().css( "left" ): oCtrl["0"].style.left;
     control.width  = oCtrl["0"].style.width;
     control.height = oCtrl["0"].style.height;
     
     return control;
  }

  function Copy()
  {
     editor.focus();
     document.execCommand('copy');
  }

  function Paste()
  {
     editor.focus();
     document.execCommand('paste');
  }

  function Clear()
  {
     $('#output').html('');
     sessionStorage.setItem( 'result', '' );
  }

  function SelectAll()
  {
     editor.selectAll();
  }

  function AceGotoLine( cLine )
  {
     editor.gotoLine( parseInt( cLine ), 0, true );
  }
  
  function GetParams()
  {
     MsgGet( 'params:  ', 'params to provide', 'SaveParams(', sessionStorage.getItem( 'params' ) );
  }
  
  function SaveParams( cParams )
  {
     sessionStorage.setItem( 'params', cParams );
  }   
   </script>  

</head>

views/menu.view

				<nav class="navbar navbar-expand-lg navbar-light bg-light navbar-dark bg-dark" style="height:35px;">
					<a class="navbar-brand" style="cursor:pointer;color:aquamarine;" 
					   onclick="location.href='https://fivetechsoft.github.io/mod_harbour/'">
						<i class="fas fa-bars fa-1x" style="padding-top:5px"></i></a>
					<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
				        <ul class="navbar-nav">
				           <li class="nav-item active dropdown">
							  <a class="nav-link dropdown-toggle" href="http://example.com" id="menuFile" 
							     data-toggle="dropdown">File</a>
				              <div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuFile">
				                 <a class="dropdown-item" style="color:white;" onclick="OpenPrg()">Open...</a> 
				                 <a class="dropdown-item" >Save</a> 
				                 <a class="dropdown-item" >Save as...</a>
				                 <div class="dropdown-divider"></div>
				                 <a class="dropdown-item" onclick="ClosePrg()">Close</a>
				                 <a class="dropdown-item" >Close all</a>
				              </div>
			               </li>
			            </ul>					
				        <ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuEdit" 
									data-toggle="dropdown">Edit</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuEdit">
									<a class="dropdown-item" style="color:white;">Cut</a> 
									<a class="dropdown-item" onclick="Copy()">Copy</a> 
									<a class="dropdown-item" onclick="Paste()">Paste</a>
									<a class="dropdown-item" onclick="Clear()">Clear</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="SelectAll()">Select all</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" 
									   onclick="MsgGet( 'number:  ', 'Goto line', 'AceGotoLine(' );" >Go to Line...</a>
									<a class="dropdown-item" >Find...</a>
									<a class="dropdown-item" >Find again</a>
									<a class="dropdown-item" >Replace and find again</a>
									<a class="dropdown-item" >Replace all</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="editor.findNext()">Next</a>
									<a class="dropdown-item" onclick="editor.findPrevious()">Prev</a>
								</div>
							</li>
						</ul>					
				        <ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuDatabase" 
									data-toggle="dropdown">DataBase</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuDatabase">
									<a class="dropdown-item" style="color:white;">New...</a> 
									<a class="dropdown-item" onclick="OpenDbf()">Open...</a> 
									<a class="dropdown-item" >Workareas...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Append from...</a>
									<a class="dropdown-item" >Copy to...</a>
									<a class="dropdown-item" >Sort...</a>
									<a class="dropdown-item" >Total...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Average...</a>
									<a class="dropdown-item" >Count...</a>
									<a class="dropdown-item" >Sum...</a>
									<a class="dropdown-item" >Calculate...</a>
									<a class="dropdown-item" >Print report...</a>
									<a class="dropdown-item" >Print label...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Pack</a>
									<a class="dropdown-item" >Reindex</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Close All</a>
								</div>
							</li>
						</ul>					
						<ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuRecord" 
									data-toggle="dropdown">Record</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuRecord">
									<a class="dropdown-item" style="color:white;">Append</a> 
									<a class="dropdown-item" style="color:white;">Change</a> 
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Goto...</a> 
									<a class="dropdown-item" >Locate...</a> 
									<a class="dropdown-item" >Continue</a> 
									<a class="dropdown-item" >Seek...</a> 
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Replace</a> 
									<a class="dropdown-item" >Delete</a> 
									<a class="dropdown-item" >Recall</a> 
								</div>
							</li>
						</ul>					
						<ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuProgram" 
									data-toggle="dropdown">Program</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuProgram">
									<a class="dropdown-item" style="color:white;">Open...</a> 
									<a class="dropdown-item" >Save</a> 
									<a class="dropdown-item" >Save as...</a>
								</div>
							</li>
						</ul>					
						<ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuRun" 
									data-toggle="dropdown">Run</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuRun">
									<a class="dropdown-item" onclick="Run()">Execute</a> 
									<a class="dropdown-item" >Debug...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="GetParams()">Params...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" >Build HRB...</a>
								</div>
							</li>
						</ul>					
						<ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuWindow" 
									data-toggle="dropdown">Window</a>
								<div class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuWindow">
									<a class="dropdown-item" style="color:white;" onclick="Command()">Command...</a> 
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="AddLabel()">Add Say</a>
									<a class="dropdown-item" onclick="AddInput()">Add Get</a>
									<a class="dropdown-item" onclick="AddButton()">Add Button</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="ShowHtml()">Show HTML...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item" onclick="CloseAllForms()">Close all</a>
								</div>
							</li>
						</ul>					
						<ul class="navbar-nav">
							<li class="nav-item active dropdown">
								<a class="nav-link dropdown-toggle" href="http://example.com" id="menuHelp" 
									data-toggle="dropdown">Help</a>
								<div id="popup8" class="dropdown-menu dropdown-menu-left bg-dark" aria-labelledby="menuHelp">
									<a class="dropdown-item" style="color:white;" href="https://github.com/FiveTechSoft/mod_harbour/wiki">mod_harbour wiki...</a> 
									<a class="dropdown-item" style="color:white;" href="https://app.slack.com/client/TJH5YU202/CJUHF8SBX">mod_harbour live chat...</a> 
									<a class="dropdown-item" style="color:white;" href="http://forums.fivetechsupport.com/viewforum.php?f=45&start=0">mod_harbour forums...</a>
									<div class="dropdown-divider"></div>
									<a class="dropdown-item"  style="color:white;" href="https://harbour.github.io">The Harbour Project</a>
								</div>
							</li>
						</ul>					
					</div>
				</nav>

			<script>
				$('.dropdown-item').bind( 'mousedown', false );
           
           $(".dropdown").hover(            
              function() {
                 $('.dropdown-menu', this).not('.in .dropdown-menu').stop(true,true).show();
                 $(this).toggleClass('open');        
              },
              function() {
                 $('.dropdown-menu', this).not('.in .dropdown-menu').stop(true,true).hide();
                 $(this).toggleClass('open');       
              }
           );
			</script>

views/code.view

   

          <div id="editor">{{GetCode()}}</div>
          <script src="https://fivetechsoft.github.io/xcloud/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
          <script>
             var editor = ace.edit("editor");
             var code;
             editor.setTheme("ace/theme/tomorrow_night_blue");
             editor.setFontSize(18);     

             editor.setHighlightActiveLine(true);
             editor.session.setMode("ace/mode/c_cpp");

         code = sessionStorage.getItem('modpro_code');  
         if( code && code.length != 0 )
            editor.session.setValue( code );
      </script>

run.prg

function Main()

   local hPost := UPost()
   local cCode
   

   if hb_HHasKey( hPost, "source" )
      cCode = hPost[ "source" ]
   else
      ? "This example is used from samples/modpro/modpro.prg"
      return nil
   endif   
   

   UAddHeader( "Access-Control-Allow-Origin", "*" )
   

   if ! Empty( cCode )
      UExecute( cCode, hPost[ "params" ] )
   endif
   

return ""
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 14, 2025 11:11 AM

calculator.prg

#include "hix.ch"

function main()
    local hParams := UGetParams() 
    local nA := 0
    local nB := 0
    local cOp := ""
    local nResult := 0
    local cError := ""
    local cHtml := ""

local aPairs
local cPair
local nPos
local cQuery

if ValType( hParams ) != 'H'
    hParams := {=>}
endif

if !hb_HHasKey( hParams, 'op' ) .and. hb_HHasKey( hParams, 'QUERY_STRING' )
    cQuery := hParams['QUERY_STRING']
    aPairs := hb_ATokens( cQuery, "&" )
    
    for each cPair in aPairs
        nPos := At( "=", cPair )
        if nPos > 0
            hParams[ Left( cPair, nPos - 1 ) ] := SubStr( cPair, nPos + 1 )
        endif
    next
endif



// Check if it's an AJAX request for calculation
if hb_HHasKey( hParams, 'op' )
    cOp := hParams[ 'op' ]

    // Safe extraction of A
    if hb_HHasKey( hParams, 'a' )
        if ValType( hParams[ 'a' ] ) == 'C'
            nA := Val( hParams[ 'a' ] )
        elseif ValType( hParams[ 'a' ] ) == 'N'
            nA := hParams[ 'a' ]
        endif
    endif
  
    // Safe extraction of B
    if hb_HHasKey( hParams, 'b' )
        if ValType( hParams[ 'b' ] ) == 'C'
            nB := Val( hParams[ 'b' ] )
        elseif ValType( hParams[ 'b' ] ) == 'N'
            nB := hParams[ 'b' ]
        endif
    endif

    nResult := 0
    cError := ""

    do case
        case cOp == 'add'
            nResult := nA + nB
        case cOp == 'sub'
            nResult := nA - nB
        case cOp == 'mul'
            nResult := nA * nB
        case cOp == 'div'
            if nB != 0
                nResult := nA / nB
            else
                cError := "Error"
            endif
            otherwise
            cError := "Invalid Op"
    endcase

    if Empty( cError )
        cHtml := Ltrim( Str( nResult ) )
    else
        cHtml := cError
    endif
else
    // Serve the embedded UI
    TEXT TO cHtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lisa Calculator</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');

    :root {
        --glass-bg: rgba(255, 255, 255, 0.1);
        --glass-border: rgba(255, 255, 255, 0.2);
        --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
        --text-color: #ffffff;
        --btn-bg: rgba(255, 255, 255, 0.05);
        --btn-hover: rgba(255, 255, 255, 0.2);
        --accent-color: rgba(100, 200, 255, 0.3);
    }

    body {
        margin: 0;
        padding: 0;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        font-family: 'Inter', sans-serif;
        background: linear-gradient(135deg, #1f1c2c 0%, #928dab 100%);
        overflow: hidden;
        color: var(--text-color);
    }

    /* Abstract blobs for background */
    .blob {
        position: absolute;
        border-radius: 50%;
        filter: blur(80px);
        opacity: 0.6;
        z-index: 0;
    }
    .blob-1 { background: #ff00cc; width: 400px; height: 400px; top: -100px; left: -100px; }
    .blob-2 { background: #3333ff; width: 500px; height: 500px; bottom: -150px; right: -150px; }
    .blob-3 { background: #00ffff; width: 300px; height: 300px; top: 40%; left: 40%; }

    .window {
        z-index: 10;
        background: var(--glass-bg);
        backdrop-filter: blur(16px);
        -webkit-backdrop-filter: blur(16px);
        border: 1px solid var(--glass-border);
        border-radius: 24px;
        box-shadow: var(--glass-shadow);
        width: 340px;
        padding: 20px;
        position: absolute;
        top: 20%;
        left: 30%;
        display: flex;
        flex-direction: column;
        transition: all 0.3s ease;
    }

    .title-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding-bottom: 20px;
        cursor: move;
    }

    .title-text {
        font-size: 18px;
        font-weight: 600;
        letter-spacing: 0.5px;
        text-shadow: 0 0 10px rgba(255,255,255,0.3);
    }

    .window-controls {
        display: flex;
        gap: 8px;
    }
    .control { width: 12px; height: 12px; border-radius: 50%; opacity: 0.8; }
    .close { background: #ff5f56; }
    .min { background: #ffbd2e; }
    .max { background: #27c93f; }

    #display {
        width: 100%;
        height: 70px;
        background: rgba(0, 0, 0, 0.2);
        border-radius: 16px;
        border: none;
        margin-bottom: 20px;
        text-align: right;
        font-size: 36px;
        line-height: 70px;
        padding-right: 20px;
        box-sizing: border-box;
        font-family: 'Inter', sans-serif;
        color: #fff;
        box-shadow: inset 2px 2px 5px rgba(0,0,0,0.1);
    }

    .buttons {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 12px;
    }

    button {
        background: var(--btn-bg);
        border: 1px solid var(--glass-border);
        border-radius: 12px;
        height: 55px;
        font-family: inherit;
        font-size: 20px;
        font-weight: 400;
        color: #fff;
        cursor: pointer;
        transition: all 0.2s ease;
        backdrop-filter: blur(4px);
    }

    button:hover {
        background: var(--btn-hover);
        transform: translateY(-2px);
        box-shadow: 0 5px 15px rgba(0,0,0,0.2);
        border-color: rgba(255,255,255,0.4);
        text-shadow: 0 0 8px rgba(255,255,255,0.6);
    }

    button:active {
        transform: scale(0.95);
    }

    button.op {
        background: rgba(255, 255, 255, 0.1);
        color: #a0d8ef;
        font-weight: 600;
    }

    button.clear {
        background: rgba(255, 100, 100, 0.2);
        color: #ffcccc;
    }
    
    button.equals {
        background: var(--accent-color);
        grid-row: span 2;
        height: auto;
        border: 1px solid rgba(100, 200, 255, 0.5);
        box-shadow: 0 0 15px rgba(100, 200, 255, 0.2);
    }

    .glow {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        border-radius: 20px;
        pointer-events: none;
        background: radial-gradient(circle at 50% 0%, rgba(255,255,255,0.1), transparent 70%);
    }

</style>
</head>
<body>

<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>

<div class="window">
    <div class="glow"></div>
    <div class="title-bar">
        <span class="title-text">GlassCalc</span>
        <div class="window-controls">
            <div class="control close"></div>
            <div class="control min"></div>
            <div class="control max"></div>
        </div>
    </div>

<div id="display">0</div>

<div class="buttons">
    <button class="clear" onclick="clearCalc()">C</button>
    <button onclick="setOp('div')" class="op">/</button>
    <button onclick="setOp('mul')" class="op">*</button>
    <button onclick="backspace()">⌫</button>

    <button onclick="append('7')">7</button>
    <button onclick="append('8')">8</button>
    <button onclick="append('9')">9</button>
    <button onclick="setOp('sub')" class="op">-</button>

    <button onclick="append('4')">4</button>
    <button onclick="append('5')">5</button>
    <button onclick="append('6')">6</button>
    <button onclick="setOp('add')" class="op">+</button>

    <button onclick="append('1')">1</button>
    <button onclick="append('2')">2</button>
    <button onclick="append('3')">3</button>
    <button class="equals" onclick="calculate()">=</button>

    <button onclick="append('0')" style="grid-column: span 2;">0</button>
    <button onclick="append('.')">.</button>
</div>
</div>

<script>
    let currentInput = "";
    let previousInput = "";
    let currentOp = "";
    let resetNext = false;
    let isError = false;

const display = document.getElementById('display');

function updateDisplay(val) {
    display.innerText = val === "" ? "0" : val;
}

function append(val) {
    if (isError) clearCalc();
    if (resetNext) {
        currentInput = "";
        resetNext = false;
    }
    if (val === '.' && currentInput.includes('.')) return;
    if (currentInput === "" && val === ".") currentInput = "0";
    
    currentInput += val;
    updateDisplay(currentInput);
}

function backspace() {
    if (isError || resetNext) return;
    currentInput = currentInput.slice(0, -1);
    updateDisplay(currentInput);
}

function clearCalc() {
    currentInput = "";
    previousInput = "";
    currentOp = "";
    resetNext = false;
    isError = false;
    updateDisplay("");
}

function setOp(op) {
    if (currentInput === "" && previousInput === "") return;
    
    if (previousInput !== "" && currentInput !== "") {
        calculate(true);     
    } else {
        if (currentInput !== "") {
             previousInput = currentInput;
             currentInput = "";
        }
    }
    currentOp = op;
    resetNext = false; 
}

function calculate(isIntermediate = false) {
    if (previousInput === "" || currentInput === "" || currentOp === "") return;

    const a = previousInput;
    const b = currentInput;
    const op = currentOp;
    
    fetch('lisa_calc.prg?op=' + op + '&a=' + a + '&b=' + b)
    .then(response => response.text())
    .then(text => {
        if (text === "Error" || text === "Invalid Op") {
            isError = true;
            currentInput = "";
            previousInput = "";
            currentOp = "";
            updateDisplay(text);
        } else {
            updateDisplay(text);
            previousInput = text;
            currentInput = ""; 
            if (!isIntermediate) {
                currentInput = text;
                resetNext = true;
                previousInput = "";
                currentOp = "";
            } else {
                resetNext = false; 
            }
        }
    })
    .catch(err => {
        console.error(err);
        updateDisplay("Sys Error");
        isError = true;
    });
}

// Drag Logic
const windowEl = document.querySelector('.window');
const titleBar = document.querySelector('.title-bar');
let isDragging = false;
let startX, startY, initialLeft, initialTop;

titleBar.addEventListener('mousedown', (e) => {
    isDragging = true;
    startX = e.clientX;
    startY = e.clientY;
    const rect = windowEl.getBoundingClientRect();
    initialLeft = rect.left;
    initialTop = rect.top;
});

document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    windowEl.style.left = (initialLeft + dx) + 'px';
    windowEl.style.top = (initialTop + dy) + 'px';
});

document.addEventListener('mouseup', () => {
    isDragging = false;
});

</script>
</body>
</html>
        ENDTEXT
    endif

UWrite( cHtml )

return ""
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 14, 2025 12:53 PM

login.prg

#include "hix.ch"

function main()
    local hParams := UGetParams() 
    local cAction := ""
    local cUser := ""
    local cPass := ""
    local cJson := ""
    local cHtml := ""
    

if ValType( hParams ) != 'H'
    hParams := {=>}
endif



// Parse QUERY_STRING if needed
if !hb_HHasKey( hParams, 'action' ) .and. hb_HHasKey( hParams, 'QUERY_STRING' )
    ParseQueryString(@hParams, hParams['QUERY_STRING'])
endif

if hb_HHasKey( hParams, 'action' )
    cAction := hParams['action']
    
    if cAction == 'login'
        if hb_HHasKey( hParams, 'u' )
            cUser := hParams['u']
        endif
        if hb_HHasKey( hParams, 'p' )
            cPass := hParams['p']
        endif
        
        // Mock authentication
        if cUser == 'admin' .and. cPass == 'admin'
            cJson := '{"status": "success", "user": "Admin"}'
        elseif cUser == 'user' .and. cPass == '1234'
            cJson := '{"status": "success", "user": "User"}'
        else
            cJson := '{"status": "error", "message": "Invalid username or password"}'
        endif
        
        UWrite(cJson)
    endif
else
    // Serve the embedded UI
    TEXT TO cHtml
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HIX Login</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');

    :root {
        --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        --glass-bg: rgba(255, 255, 255, 0.1);
        --glass-border: 1px solid rgba(255, 255, 255, 0.2);
        --text-color: #ffffff;
        --input-bg: rgba(255, 255, 255, 0.05);
        --input-focus: rgba(255, 255, 255, 0.15);
    }

    body {
        margin: 0;
        padding: 0;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        font-family: 'Inter', sans-serif;
        background: #0f0c29;
        background: -webkit-linear-gradient(to right, #24243e, #302b63, #0f0c29);
        background: linear-gradient(to right, #24243e, #302b63, #0f0c29);
        overflow: hidden;
        color: var(--text-color);
    }

    /* Animated background blob */
    .blob {
        position: absolute;
        width: 500px;
        height: 500px;
        background: var(--primary-gradient);
        border-radius: 50%;
        filter: blur(80px);
        z-index: 0;
        animation: moveBlob 10s infinite alternate;
        opacity: 0.6;
    }

    @keyframes moveBlob {
        from { transform: translate(-20%, -20%) scale(1); }
        to { transform: translate(20%, 20%) scale(1.1); }
    }

    .login-card {
        position: relative;
        z-index: 10;
        width: 380px;
        padding: 40px;
        background: var(--glass-bg);
        backdrop-filter: blur(20px);
        -webkit-backdrop-filter: blur(20px);
        border-radius: 20px;
        border: var(--glass-border);
        box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
        display: flex;
        flex-direction: column;
        gap: 25px;
        transition: transform 0.3s ease;
    }

    .login-card:hover {
        transform: translateY(-5px);
    }

    h2 {
        margin: 0;
        font-weight: 600;
        font-size: 28px;
        text-align: center;
        margin-bottom: 10px;
        letter-spacing: 1px;
    }

    p {
        margin: 0;
        text-align: center;
        font-size: 14px;
        opacity: 0.7;
        margin-bottom: 20px;
    }

    .input-group {
        position: relative;
    }

    .input-group input {
        width: 100%;
        padding: 15px;
        background: var(--input-bg);
        border: var(--glass-border);
        border-radius: 10px;
        color: var(--text-color);
        font-size: 16px;
        font-family: inherit;
        outline: none;
        box-sizing: border-box; /* Important for padding */
        transition: all 0.3s ease;
    }

    .input-group input:focus {
        background: var(--input-focus);
        border-color: rgba(255, 255, 255, 0.5);
        box-shadow: 0 0 15px rgba(118, 75, 162, 0.3);
    }

    .input-group input::placeholder {
        color: rgba(255, 255, 255, 0.5);
    }

    button.submit-btn {
        padding: 15px;
        background: var(--primary-gradient);
        border: none;
        border-radius: 10px;
        color: white;
        font-size: 16px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        text-transform: uppercase;
        letter-spacing: 1px;
    }

    button.submit-btn:hover {
        transform: scale(1.02);
        box-shadow: 0 6px 20px rgba(118, 75, 162, 0.4);
    }
    
    button.submit-btn:active {
        transform: scale(0.98);
    }

    .footer {
        text-align: center;
        font-size: 12px;
        opacity: 0.5;
        margin-top: 10px;
    }

    /* Spinner */
    .spinner {
        display: none;
        width: 20px;
        height: 20px;
        border: 3px solid rgba(255,255,255,0.3);
        border-radius: 50%;
        border-top-color: #fff;
        animation: spin 1s ease-in-out infinite;
        margin: 0 auto;
    }

    @keyframes spin {
        to { transform: rotate(360deg); }
    }

    /* Shake animation for error */
    .shake {
        animation: shake 0.5s;
    }

    @keyframes shake {
        0% { transform: translate(1px, 1px) rotate(0deg); }
        10% { transform: translate(-1px, -2px) rotate(-1deg); }
        20% { transform: translate(-3px, 0px) rotate(1deg); }
        30% { transform: translate(3px, 2px) rotate(0deg); }
        40% { transform: translate(1px, -1px) rotate(1deg); }
        50% { transform: translate(-1px, 2px) rotate(-1deg); }
        60% { transform: translate(-3px, 1px) rotate(0deg); }
        70% { transform: translate(3px, 1px) rotate(-1deg); }
        80% { transform: translate(-1px, -1px) rotate(1deg); }
        90% { transform: translate(1px, 2px) rotate(0deg); }
        100% { transform: translate(1px, -2px) rotate(-1deg); }
    }
    
    .error-msg {
        color: #ff6b6b;
        font-size: 14px;
        text-align: center;
        min-height: 20px;
        opacity: 0;
        transition: opacity 0.3s;
    }
</style>
</head>
<body>

<div class="blob"></div>

<div class="login-card" id="loginCard">
    <h2>Welcome Back</h2>
    <p>Enter your credentials to access HIX</p>
    
    <div class="input-group">
        <input type="text" id="username" placeholder="Username" autocomplete="off">
    </div>
    
    <div class="input-group">
        <input type="password" id="password" placeholder="Password">
    </div>

    <div class="error-msg" id="errorMsg">Invalid credentials</div>

    <button class="submit-btn" onclick="attemptLogin()">
        <span id="btnText">Log In</span>
        <div class="spinner" id="btnSpinner"></div>
    </button>

    <div class="footer">Powered by HIX &copy; 2025</div>
</div>

<script>
    async function attemptLogin() {
        const user = document.getElementById('username').value;
        const pass = document.getElementById('password').value;
        const btnText = document.getElementById('btnText');
        const btnSpinner = document.getElementById('btnSpinner');
        const errorMsg = document.getElementById('errorMsg');
        const loginCard = document.getElementById('loginCard');

        if(!user || !pass) {
             showError("Please fill in all fields");
             return;
        }

        // Reset UI
        errorMsg.style.opacity = '0';
        btnText.style.display = 'none';
        btnSpinner.style.display = 'block';

        try {
            // Simulate network delay for effect
            // await new Promise(r => setTimeout(r, 800));

            const response = await fetch('login.prg?action=login&u=' + encodeURIComponent(user) + '&p=' + encodeURIComponent(pass));
            
            // HIX returns text generally, we parse it
            const text = await response.text();
            let data;
            try {
                data = JSON.parse(text);
            } catch(e) {
                console.error("JSON Parse error", text);
                throw new Error("Invalid server response");
            }

            if (data.status === 'success') {
                // Success animation or redirect
                document.body.innerHTML = '<h1 style="color:white; font-weight:300;">Welcome, ' + data.user + '</h1>';
            } else {
                showError(data.message || "Login failed");
            }

        } catch (err) {
            console.error(err);
            showError("System connection error");
        } finally {
            btnText.style.display = 'inline';
            btnSpinner.style.display = 'none';
        }
    }

    function showError(msg) {
        const errorMsg = document.getElementById('errorMsg');
        const loginCard = document.getElementById('loginCard');
        
        errorMsg.innerText = msg;
        errorMsg.style.opacity = '1';
        
        loginCard.classList.remove('shake');
        void loginCard.offsetWidth; // trigger reflow
        loginCard.classList.add('shake');
    }

    // Enter key support
    document.getElementById('password').addEventListener('keypress', function (e) {
        if (e.key === 'Enter') {
            attemptLogin();
        }
    });
</script>
</body>
</html>
        ENDTEXT
        

    UWrite(cHtml)
endif

return ""

// Helper to parse query string manually if framework doesn't do it automatically for some inputs
procedure ParseQueryString(hParams, cQuery)
    local aPairs := hb_ATokens( cQuery, "&" )
    local cPair, nPos
    

for each cPair in aPairs
    nPos := At( "=", cPair )
    if nPos > 0
        hParams[ Left( cPair, nPos - 1 ) ] := SubStr( cPair, nPos + 1 )
    endif
next
return
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 1445
Joined: Mon Oct 10, 2005 02:38 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sun Dec 14, 2025 01:23 PM

Antonio,

8-6 = <html><body><h1>404 Not Found</h1></body></html>
Me falta algo?
Main() debe tener parámetros?

Me gustaba más el código de la primera versión; tenía la parte visual en el cHtml := View( "calculator", hParams ), que venía a ser algo parecido a un "#include", cierto?
Veo que has substituído uPost() por UGetParams() (que no sé como se hace el Set... ) y ahora hay un UWrite( cHtml ) final.

Entiendo que UWrite( cHtml ) hace que Hix envíe a pantalla y permite seguir 'haciendo cosas' en la misma función; e incluso que Hix envíe avisos a pantalla mientras Hix continua trabajando con el resto de lógica de la función, es así?

En fin ya explicarás que hace qué y si es posible usar algo parecido al "#include" para separar lógica del visual, o insertar definiciones de variables con valores específicos, ...

Un Saludo

Carlos G.



FiveWin 25.12 + Harbour 3.2.0dev (r2502110321), BCC 7.7 Windows 11 Home

Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 15, 2025 03:56 AM

Carlos,

Ambos login.prg y calculator.prg te dan ese error ?

Le he pedido a Charly que revise estos ejemplos nuevos por si hay algo que no sea correcto.

Aqui funcionan ambos bien :)

regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 15, 2025 04:14 AM

Carlos,

Encontrado el error. modifica esta línea:

fetch('lisa_calc.prg?op=' + op + '&a=' + a + '&b=' + b)

por

fetch('calculator.prg?op=' + op + '&a=' + a + '&b=' + b)

regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 1445
Joined: Mon Oct 10, 2005 02:38 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 15, 2025 06:57 AM
Antonio Linares wrote:

Carlos,

Ambos login.prg y calculator.prg te dan ese error ?

Le he pedido a Charly que revise estos ejemplos nuevos por si hay algo que no sea correcto.

Aqui funcionan ambos bien :)

Sólo falla calculator.prg

Un Saludo

Carlos G.



FiveWin 25.12 + Harbour 3.2.0dev (r2502110321), BCC 7.7 Windows 11 Home

Posts: 1445
Joined: Mon Oct 10, 2005 02:38 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Mon Dec 15, 2025 06:58 AM
Antonio Linares wrote:

Carlos,

Encontrado el error. modifica esta línea:

fetch('lisa_calc.prg?op=' + op + '&a=' + a + '&b=' + b)

por

fetch('calculator.prg?op=' + op + '&a=' + a + '&b=' + b)

Resuelto, funcionando. Y además resta bien. ;)

Un Saludo

Carlos G.



FiveWin 25.12 + Harbour 3.2.0dev (r2502110321), BCC 7.7 Windows 11 Home

Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Fri Dec 19, 2025 07:29 PM

agenda.prg

function Main()
    

local cAction    := Upost( "action" )
local cId        := Upost( "id" )
local cFirstname := Upost( "firstname" )
local cLastname  := Upost( "lastname" )
local cPhone     := Upost( "phone" )
local cEmail     := Upost( "email" )

// Search Parameter (Post)
local cSearch    := Upost( "search" )

local cDbPath    := "c:/hix/testdir/agenda.dbf"
local cTmpDir    := "c:/hix/testdir"
local cMessage   := ""

// Ensure Directory Exists
if ! IsDirectory( cTmpDir )
    DirMake( cTmpDir )
endif

if ! File( cDbPath )
    DbCreate( cDbPath, { ;
        { "ID",        "C",  8, 0 }, ;
        { "FIRSTNAME", "C", 30, 0 }, ;
        { "LASTNAME",  "C", 30, 0 }, ;
        { "PHONE",     "C", 20, 0 }, ;
        { "EMAIL",     "C", 50, 0 } ;
        } )
endif

// Robust Opening Logic
if Select("contacts") > 0
    select contacts
else
    select 0
    use (cDbPath) shared alias "contacts"
    if NetErr()
        UWrite( "Error opening database.<br>" )
        return ""
    endif
endif

set deleted on

do case
    case cAction == "save"
        if !Empty(cFirstname) .and. !Empty(cLastname)
            select contacts
            if Empty( cId ) 
                append blank
                replace id with GenerateID()
                replace firstname with cFirstname
                replace lastname  with cLastname
                replace phone     with cPhone
                replace email     with cEmail
                commit
                unlock
                cMessage := "Contact saved successfully."
            else
                // Update existing
                go top
                locate for id == cId
                if found()
                    if Rlock()
                        replace firstname with cFirstname
                        replace lastname  with cLastname
                        replace phone     with cPhone
                        replace email     with cEmail
                        commit
                        unlock
                        cMessage := "Contact updated successfully."
                    endif
                else
                    // ID not found? Treat as new.
                    append blank
                    replace id with GenerateID()
                    replace firstname with cFirstname
                    replace lastname  with cLastname
                    replace phone     with cPhone
                    replace email     with cEmail
                    commit
                    unlock
                    cMessage := "Contact saved successfully."
                endif
            endif
        endif
        cId := "" 
        cFirstname := ""
        cLastname := ""
        cPhone := ""
        cEmail := ""
        // Clear search on save
        cSearch := ""
        
    case cAction == "delete"
        if ! Empty( cId )
            select contacts
            go top
            locate for id == cId
            if found()
                if Rlock()
                    delete
                    unlock
                    commit
                    cMessage := "Contact deleted successfully."
                else
                    cMessage := "Error: Could not lock record (" + AllTrim(cId) + ") for deletion."
                endif
            else
                cMessage := "Error: Contact ID (" + AllTrim(cId) + ") not found."
            endif
        else
            cMessage := "Error: No ID provided for deletion."
        endif
        cId := ""
        cSearch := "" // Clear search context on delete

    case cAction == "edit"
        select contacts
        go top
        locate for id == cId
        if found()
            cFirstname := contacts->firstname
            cLastname  := contacts->lastname
            cPhone     := contacts->phone
            cEmail     := contacts->email
        endif
        
    case cAction == "cancel"
        cId := ""
        cFirstname := ""
        cLastname := ""
        cPhone := ""
        cEmail := ""
        cSearch := ""
    
    case cAction == "search"
        // Handled by default read of cSearch parameter logic
        // No specific logic needed here, just don't clear it.

endcase

RenderView( cId, cFirstname, cLastname, cPhone, cEmail, cMessage, cSearch )

select contacts
use

return ""

function RenderView( cId, cFirst, cLast, cPhone, cEmail, cMessage, cSearch )
    local cHtml := MemoRead( "c:/hix/agenda.view" )
    local cRows := ""
    local cFormAction := "agenda.prg"
    local cMsgHtml := ""
    local lMatch := .T.
    local cFullData := ""

if Empty( cHtml )
    UWrite( "Error: Could not read agenda.view template." )
    return ""
endif

select contacts
go top

do while ! Eof()
    lMatch := .T.
    
    // Search Filter Logic
    if !Empty( cSearch )
        cFullData := Upper(contacts->firstname + " " + contacts->lastname + " " + contacts->phone + " " + contacts->email)
        if ! ( Upper(AllTrim(cSearch)) $ cFullData )
            lMatch := .F.
        endif
    endif

    if lMatch
        cRows += [<tr>]
        cRows += [<td>] + AllTrim( contacts->firstname ) + [ ] + AllTrim( contacts->lastname ) + [</td>]
        cRows += [<td>] + AllTrim( contacts->phone ) + [</td>]
        cRows += [<td class="actions-cell">]
        
        // POST Form for Edit
        cRows += [<form method="POST" action="] + cFormAction + [" style="display:inline;">]
        cRows += [<input type="hidden" name="action" value="edit">]
        cRows += [<input type="hidden" name="id" value="] + AllTrim(contacts->id) + [">]
        cRows += [<input type="hidden" name="search" value="] + cSearch + [">] 
        
        cRows += [<button type="submit" class="btn btn-sm btn-edit"><i class="fa-solid fa-pen"></i></button>]
        cRows += [</form> ]
        
        // POST Form for Delete
        cRows += [<form method="POST" action="] + cFormAction + [" style="display:inline;" onsubmit="return confirm('Delete contact?')">]
        cRows += [<input type="hidden" name="action" value="delete">]
        cRows += [<input type="hidden" name="id" value="] + AllTrim(contacts->id) + [">]
        cRows += [<input type="hidden" name="search" value="] + cSearch + [">]
        cRows += [<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>]
        cRows += [</form>]
        
        cRows += [</td></tr>]
    endif
    skip
enddo

if Empty( cRows )
    if !Empty( cSearch )
        cRows := [<tr><td colspan="3" class="empty-state">No contacts found matching "] + cSearch + [".</td></tr>]
    else
        cRows := [<tr><td colspan="3" class="empty-state">No contacts found. Add one above!</td></tr>]
    endif
endif

// Process Message
if !Empty( cMessage )
    // Determine type of alert
    // If message starts with "Error", use alert-danger (red)
    if Left( cMessage, 5 ) == "Error"
        cMsgHtml := [<div class="alert alert-danger" style="color: #f87171; background-color: rgba(248, 113, 113, 0.2); border: 1px solid rgba(248, 113, 113, 0.3);">] + cMessage + [</div>]
    else
        cMsgHtml := [<div class="alert alert-success">] + cMessage + [</div>]
    endif
endif

cHtml := StrTran( cHtml, "{{ACTION}}", cFormAction )
cHtml := StrTran( cHtml, "{{VAL_ID}}", iif( Empty(cId), "", cId ) )
cHtml := StrTran( cHtml, "{{VAL_FIRSTNAME}}", iif( Empty(cFirst), "", cFirst ) )
cHtml := StrTran( cHtml, "{{VAL_LASTNAME}}", iif( Empty(cLast), "", cLast ) )
cHtml := StrTran( cHtml, "{{VAL_PHONE}}", iif( Empty(cPhone), "", cPhone ) )
cHtml := StrTran( cHtml, "{{VAL_EMAIL}}", iif( Empty(cEmail), "", cEmail ) )
cHtml := StrTran( cHtml, "{{CONTACTS_ROWS}}", cRows )
cHtml := StrTran( cHtml, "{{MESSAGE}}", cMsgHtml )
cHtml := StrTran( cHtml, "{{VAL_SEARCH}}", iif( Empty(cSearch), "", cSearch ) )

// Add title dynamic
if !Empty(cId)
    cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "Edit Contact" )
    cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", [<a href="] + cFormAction + [?action=cancel" class="btn" style="background-color: #475569; color: white; margin-top: 0.5rem; justify-content:center;">Cancel Update</a>] )
else
    cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "Add Contact" )
    cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", "" )
endif

UWrite( cHtml )

return ""

function GenerateID()
    local cChars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    local cId := ""
    local i
    for i := 1 to 8
        cId += SubStr( cChars, Int( hb_Random() * Len(cChars) ) + 1, 1 )
    next
return cId

agenda.view

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HIX Agenda</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #6366f1;
            --primary-hover: #4f46e5;
            --danger-color: #ef4444;
            --bg-color: #0f172a;
            --card-bg: #1e293b;
            --text-color: #f8fafc;
            --text-muted: #94a3b8;
            --input-bg: #334155;
            --border-color: #475569;
        }

    body {
        font-family: 'Outfit', sans-serif;
        background-color: var(--bg-color);
        color: var(--text-color);
        margin: 0;
        padding: 2rem;
        min-height: 100vh;
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    .container {
        width: 100%;
        max-width: 900px;
        display: grid;
        grid-template-columns: 1fr;
        gap: 2rem;
    }

    @media (min-width: 768px) {
        .container {
            grid-template-columns: 350px 1fr;
        }
    }

    h1 {
        text-align: center;
        margin-bottom: 2rem;
        font-weight: 600;
        background: linear-gradient(to right, #818cf8, #c4b5fd);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        width: 100%;
    }

    .card {
        background-color: var(--card-bg);
        padding: 1.5rem;
        border-radius: 1rem;
        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
        border: 1px solid rgba(255, 255, 255, 0.05);
        height: fit-content;
    }

    .card-header {
        font-size: 1.25rem;
        font-weight: 600;
        margin-bottom: 1.5rem;
        color: var(--text-color);
        border-bottom: 1px solid var(--border-color);
        padding-bottom: 0.5rem;
    }

    .form-group {
        margin-bottom: 1rem;
    }

    label {
        display: block;
        margin-bottom: 0.5rem;
        font-size: 0.875rem;
        color: var(--text-muted);
    }

    input {
        width: 100%;
        padding: 0.75rem;
        border-radius: 0.5rem;
        border: 1px solid var(--border-color);
        background-color: var(--input-bg);
        color: white;
        font-family: inherit;
        box-sizing: border-box;
        transition: all 0.2s;
    }

    input:focus {
        outline: none;
        border-color: var(--primary-color);
        box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
    }

    .btn {
        width: 100%;
        padding: 0.75rem;
        border: none;
        border-radius: 0.5rem;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.2s;
        display: inline-flex;
        justify-content: center;
        align-items: center;
        gap: 0.5rem;
        text-decoration: none;
        font-size: 0.9rem;
    }

    .btn-primary {
        background-color: var(--primary-color);
        color: white;
    }

    .btn-primary:hover {
        background-color: var(--primary-hover);
    }

    .btn-sm {
        padding: 0.4rem 0.8rem;
        font-size: 0.8rem;
        width: auto;
    }

    .btn-danger {
        background-color: transparent;
        color: var(--danger-color);
        border: 1px solid var(--danger-color);
    }

    .btn-danger:hover {
        background-color: var(--danger-color);
        color: white;
    }
    
    .btn-edit {
        background-color: transparent;
        color: var(--primary-color);
        border: 1px solid var(--primary-color);
    }
    
    .btn-edit:hover {
        background-color: var(--primary-color);
        color: white;
    }
    
    .btn-search {
        width: auto;
        align-self: flex-end;
        margin-bottom: 2px;
    }


    table {
        width: 100%;
        border-collapse: collapse;
        color: var(--text-color);
    }

    th, td {
        text-align: left;
        padding: 1rem;
        border-bottom: 1px solid var(--border-color);
    }

    th {
        color: var(--text-muted);
        font-weight: 600;
        font-size: 0.85rem;
        text-transform: uppercase;
        letter-spacing: 0.05em;
    }

    tr:last-child td {
        border-bottom: none;
    }
    
    .empty-state {
        text-align: center;
        padding: 2rem;
        color: var(--text-muted);
    }

    .actions-cell {
        display: flex;
        gap: 0.5rem;
        justify-content: flex-end;
    }
    
    .alert {
        padding: 1rem;
        margin-bottom: 1rem;
        border-radius: 0.5rem;
        font-weight: 500;
        text-align: center;
    }
    
    .alert-success {
        background-color: rgba(16, 185, 129, 0.2);
        color: #34d399;
        border: 1px solid rgba(16, 185, 129, 0.3);
    }
    
    .search-bar {
        display: flex; 
        gap: 0.5rem; 
        margin-bottom: 1rem;
    }

</style>
</head>
<body>

<h1><i class="fa-solid fa-address-book"></i> HIX Agenda</h1>

<div class="container">
    <!-- Form Section -->
    <div class="card">
        <div class="card-header">
            {{FORM_TITLE}}
        </div>
        
        {{MESSAGE}}
        
        <form method="POST" action="{{ACTION}}">
            <input type="hidden" name="action" value="save">
            <input type="hidden" name="id" value="{{VAL_ID}}">
            
            <div class="form-group">
                <label>First Name</label>
                <input type="text" name="firstname" value="{{VAL_FIRSTNAME}}" required placeholder="John">
            </div>

            <div class="form-group">
                <label>Last Name</label>
                <input type="text" name="lastname" value="{{VAL_LASTNAME}}" required placeholder="Doe">
            </div>

            <div class="form-group">
                <label>Phone</label>
                <input type="tel" name="phone" value="{{VAL_PHONE}}" placeholder="+1 234 567 890">
            </div>

            <div class="form-group">
                <label>Email</label>
                <input type="email" name="email" value="{{VAL_EMAIL}}" placeholder="john@example.com">
            </div>

            <button type="submit" class="btn btn-primary">
                <i class="fa-solid fa-floppy-disk"></i> Save Contact
            </button>
            
            {{CANCEL_BUTTON}}
        </form>
    </div>

    <!-- List Section -->
    <div class="card">
        <div class="card-header">
            Contacts
        </div>
        
        <form method="POST" action="{{ACTION}}" class="search-bar">
            <input type="hidden" name="action" value="search">
            <input type="text" name="search" value="{{VAL_SEARCH}}" placeholder="Search contacts..." style="flex-grow:1;">
            <button type="submit" class="btn btn-primary btn-search"><i class="fa-solid fa-magnifying-glass"></i></button>
            <a href="{{ACTION}}" class="btn btn-search" style="background: #334155;"><i class="fa-solid fa-xmark"></i></a>
        </form>
        
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Phone / Email</th>
                    <th style="text-align: right;">Actions</th>
                </tr>
            </thead>
            <tbody>
                {{CONTACTS_ROWS}}
            </tbody>
        </table>
    </div>
</div>

</body>
</html>
regards, saludos

Antonio Linares
www.fivetechsoft.com
Posts: 44158
Joined: Thu Oct 06, 2005 05:47 PM
Re: HIX &amp; AntiGravity apps gallery
Posted: Sat Dec 20, 2025 11:47 AM

Full invoicing webapp with PDFs / Facturación completa con PDFs






regards, saludos

Antonio Linares
www.fivetechsoft.com