Compare commits
10 Commits
ed98690bb6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 62af341a1e | |||
| 6019d67145 | |||
| b7e060441c | |||
| 3b964c4d9c | |||
| d66caee7fd | |||
| c5473fec5c | |||
| 201821d53c | |||
| 3be0033a32 | |||
| 07692fe0bd | |||
| 7ae5b2fbbc |
@@ -1 +0,0 @@
|
|||||||
/nix/store/cgjr3kj3hs7ngznyws5qfg16c8scpys0-bash-interactive-5.3p9
|
|
||||||
@@ -1,603 +0,0 @@
|
|||||||
# Shared library functions for devenv evaluation
|
|
||||||
{ inputs }:
|
|
||||||
|
|
||||||
rec {
|
|
||||||
# Helper to get overlays for a given input
|
|
||||||
getOverlays =
|
|
||||||
inputName: inputAttrs:
|
|
||||||
let
|
|
||||||
lib = inputs.nixpkgs.lib;
|
|
||||||
in
|
|
||||||
map
|
|
||||||
(
|
|
||||||
overlay:
|
|
||||||
let
|
|
||||||
input =
|
|
||||||
inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays.");
|
|
||||||
in
|
|
||||||
input.overlays.${overlay}
|
|
||||||
or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${lib.concatStringsSep ", " (builtins.attrNames input.overlays)}")
|
|
||||||
) inputAttrs.overlays or [ ];
|
|
||||||
|
|
||||||
# Main function to create devenv configuration for a specific system with profiles support
|
|
||||||
# This is the full-featured version used by default.nix
|
|
||||||
mkDevenvForSystem =
|
|
||||||
{ version
|
|
||||||
, is_development_version ? false
|
|
||||||
, require_version_match ? false
|
|
||||||
, system
|
|
||||||
, devenv_root
|
|
||||||
, git_root ? null
|
|
||||||
, devenv_dotfile
|
|
||||||
, devenv_dotfile_path
|
|
||||||
, devenv_tmpdir
|
|
||||||
, devenv_runtime
|
|
||||||
, devenv_state ? null
|
|
||||||
, devenv_istesting ? false
|
|
||||||
, devenv_direnvrc_latest_version
|
|
||||||
, active_profiles ? [ ]
|
|
||||||
, hostname
|
|
||||||
, username
|
|
||||||
, cli_options ? [ ]
|
|
||||||
, skip_local_src ? false
|
|
||||||
, secretspec ? null
|
|
||||||
, devenv_inputs ? { }
|
|
||||||
, devenv_imports ? [ ]
|
|
||||||
, impure ? false
|
|
||||||
, nixpkgs_config ? { }
|
|
||||||
, lock_fingerprint ? null
|
|
||||||
, primops ? { }
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (inputs) nixpkgs;
|
|
||||||
lib = nixpkgs.lib;
|
|
||||||
targetSystem = system;
|
|
||||||
|
|
||||||
overlays = lib.flatten (lib.mapAttrsToList getOverlays devenv_inputs);
|
|
||||||
|
|
||||||
# Helper to create pkgs for a given system with nixpkgs_config
|
|
||||||
mkPkgsForSystem =
|
|
||||||
evalSystem:
|
|
||||||
import nixpkgs {
|
|
||||||
system = evalSystem;
|
|
||||||
config = nixpkgs_config // {
|
|
||||||
# nixpkgs' check-meta.nix natively handles permittedInsecurePackages
|
|
||||||
# via allowInsecureDefaultPredicate using the full derivation name.
|
|
||||||
# We must NOT override allowInsecurePredicate here, as lib.getName
|
|
||||||
# strips the version, causing mismatches with user-provided entries
|
|
||||||
# like "openssl-1.1.1w".
|
|
||||||
#
|
|
||||||
# For unfree packages, nixpkgs does not natively support
|
|
||||||
# permittedUnfreePackages, so we provide a custom predicate.
|
|
||||||
allowUnfreePredicate =
|
|
||||||
if nixpkgs_config.allowUnfree or false then
|
|
||||||
(_: true)
|
|
||||||
else if (nixpkgs_config.permittedUnfreePackages or [ ]) != [ ] then
|
|
||||||
(pkg: builtins.elem (lib.getName pkg) (nixpkgs_config.permittedUnfreePackages or [ ]))
|
|
||||||
else
|
|
||||||
(_: false);
|
|
||||||
} // lib.optionalAttrs ((nixpkgs_config.allowlistedLicenses or [ ]) != [ ]) {
|
|
||||||
allowlistedLicenses = map (name: lib.licenses.${name}) (nixpkgs_config.allowlistedLicenses or [ ]);
|
|
||||||
} // lib.optionalAttrs ((nixpkgs_config.blocklistedLicenses or [ ]) != [ ]) {
|
|
||||||
blocklistedLicenses = map (name: lib.licenses.${name}) (nixpkgs_config.blocklistedLicenses or [ ]);
|
|
||||||
};
|
|
||||||
inherit overlays;
|
|
||||||
};
|
|
||||||
|
|
||||||
pkgsBootstrap = mkPkgsForSystem targetSystem;
|
|
||||||
|
|
||||||
# Helper to import a path, trying .nix first then /devenv.nix
|
|
||||||
# Returns a list of modules, including devenv.local.nix when present
|
|
||||||
tryImport =
|
|
||||||
resolvedPath: basePath:
|
|
||||||
if lib.hasSuffix ".nix" basePath then
|
|
||||||
[ (import resolvedPath) ]
|
|
||||||
else
|
|
||||||
let
|
|
||||||
devenvpath = resolvedPath + "/devenv.nix";
|
|
||||||
localpath = resolvedPath + "/devenv.local.nix";
|
|
||||||
in
|
|
||||||
if builtins.pathExists devenvpath then
|
|
||||||
[ (import devenvpath) ] ++ lib.optional (builtins.pathExists localpath) (import localpath)
|
|
||||||
else
|
|
||||||
throw (basePath + "/devenv.nix file does not exist");
|
|
||||||
|
|
||||||
importModule =
|
|
||||||
path:
|
|
||||||
if lib.hasPrefix "path:" path then
|
|
||||||
# path: prefix indicates a local filesystem path - strip it and import directly
|
|
||||||
let
|
|
||||||
actualPath = builtins.substring 5 999999 path;
|
|
||||||
in
|
|
||||||
tryImport (/. + actualPath) path
|
|
||||||
else if lib.hasPrefix "/" path then
|
|
||||||
# Absolute path - import directly (avoids input resolution and NAR hash computation)
|
|
||||||
tryImport (/. + path) path
|
|
||||||
else if lib.hasPrefix "./" path then
|
|
||||||
# Relative paths are relative to devenv_root, not bootstrap directory
|
|
||||||
let
|
|
||||||
relPath = builtins.substring 1 255 path;
|
|
||||||
in
|
|
||||||
tryImport (/. + devenv_root + relPath) path
|
|
||||||
else if lib.hasPrefix "../" path then
|
|
||||||
# Parent relative paths also relative to devenv_root
|
|
||||||
tryImport (/. + devenv_root + "/${path}") path
|
|
||||||
else
|
|
||||||
let
|
|
||||||
paths = lib.splitString "/" path;
|
|
||||||
name = builtins.head paths;
|
|
||||||
input = inputs.${name} or (throw "Unknown input ${name}");
|
|
||||||
subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}";
|
|
||||||
devenvpath = input + subpath;
|
|
||||||
in
|
|
||||||
tryImport devenvpath path;
|
|
||||||
|
|
||||||
# Common modules shared between main evaluation and cross-system evaluation
|
|
||||||
mkCommonModules =
|
|
||||||
evalPkgs:
|
|
||||||
[
|
|
||||||
(
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
_module.args.pkgs = evalPkgs.appendOverlays (config.overlays or [ ]);
|
|
||||||
_module.args.secretspec = secretspec;
|
|
||||||
_module.args.devenvPrimops = primops;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(inputs.devenv.modules + /top-level.nix)
|
|
||||||
(
|
|
||||||
{ options, ... }:
|
|
||||||
{
|
|
||||||
config.devenv = lib.mkMerge [
|
|
||||||
{
|
|
||||||
root = devenv_root;
|
|
||||||
dotfile = devenv_dotfile;
|
|
||||||
}
|
|
||||||
(
|
|
||||||
if builtins.hasAttr "cli" options.devenv then
|
|
||||||
{
|
|
||||||
cli.version = version;
|
|
||||||
cli.isDevelopment = is_development_version;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cliVersion = version;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(lib.optionalAttrs
|
|
||||||
(builtins.hasAttr "cli" options.devenv
|
|
||||||
&& builtins.hasAttr "requireVersionMatch" options.devenv.cli)
|
|
||||||
{
|
|
||||||
cli.requireVersionMatch = require_version_match;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) {
|
|
||||||
tmpdir = devenv_tmpdir;
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) {
|
|
||||||
isTesting = devenv_istesting;
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) {
|
|
||||||
runtime = devenv_runtime;
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "state" options.devenv && devenv_state != null) {
|
|
||||||
state = lib.mkForce devenv_state;
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) {
|
|
||||||
direnvrcLatestVersion = devenv_direnvrc_latest_version;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(
|
|
||||||
{ options, ... }:
|
|
||||||
{
|
|
||||||
config = lib.mkMerge [
|
|
||||||
(lib.optionalAttrs (builtins.hasAttr "git" options) {
|
|
||||||
git.root = git_root;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
++ (lib.flatten (map importModule devenv_imports))
|
|
||||||
++ (if !skip_local_src then (importModule (devenv_root + "/devenv.nix")) else [ ])
|
|
||||||
++ [
|
|
||||||
(
|
|
||||||
let
|
|
||||||
localPath = devenv_root + "/devenv.local.nix";
|
|
||||||
in
|
|
||||||
if builtins.pathExists localPath then import localPath else { }
|
|
||||||
)
|
|
||||||
cli_options
|
|
||||||
];
|
|
||||||
|
|
||||||
# Phase 1: Base evaluation to extract profile definitions
|
|
||||||
baseProject = lib.evalModules {
|
|
||||||
specialArgs = inputs // {
|
|
||||||
inherit inputs secretspec primops;
|
|
||||||
};
|
|
||||||
modules = mkCommonModules pkgsBootstrap;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Phase 2: Extract and apply profiles using extendModules with priority overrides
|
|
||||||
project =
|
|
||||||
let
|
|
||||||
# Build ordered list of profile names: hostname -> user -> manual
|
|
||||||
manualProfiles = active_profiles;
|
|
||||||
currentHostname = hostname;
|
|
||||||
currentUsername = username;
|
|
||||||
hostnameProfiles = lib.optional
|
|
||||||
(
|
|
||||||
currentHostname != null
|
|
||||||
&& currentHostname != ""
|
|
||||||
&& builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { })
|
|
||||||
) "hostname.${currentHostname}";
|
|
||||||
userProfiles = lib.optional
|
|
||||||
(
|
|
||||||
currentUsername != null
|
|
||||||
&& currentUsername != ""
|
|
||||||
&& builtins.hasAttr currentUsername (baseProject.config.profiles.user or { })
|
|
||||||
) "user.${currentUsername}";
|
|
||||||
|
|
||||||
# Ordered list of profiles to activate
|
|
||||||
orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles;
|
|
||||||
|
|
||||||
# Resolve profile extends with cycle detection
|
|
||||||
resolveProfileExtends =
|
|
||||||
profileName: visited:
|
|
||||||
if builtins.elem profileName visited then
|
|
||||||
throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}"
|
|
||||||
else
|
|
||||||
let
|
|
||||||
profile = getProfileConfig profileName;
|
|
||||||
extends = profile.extends or [ ];
|
|
||||||
newVisited = visited ++ [ profileName ];
|
|
||||||
extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends);
|
|
||||||
in
|
|
||||||
extendedProfiles ++ [ profileName ];
|
|
||||||
|
|
||||||
# Get profile configuration by name from baseProject
|
|
||||||
getProfileConfig =
|
|
||||||
profileName:
|
|
||||||
if lib.hasPrefix "hostname." profileName then
|
|
||||||
let
|
|
||||||
name = lib.removePrefix "hostname." profileName;
|
|
||||||
in
|
|
||||||
baseProject.config.profiles.hostname.${name}
|
|
||||||
else if lib.hasPrefix "user." profileName then
|
|
||||||
let
|
|
||||||
name = lib.removePrefix "user." profileName;
|
|
||||||
in
|
|
||||||
baseProject.config.profiles.user.${name}
|
|
||||||
else
|
|
||||||
let
|
|
||||||
availableProfiles = builtins.attrNames (baseProject.config.profiles or { });
|
|
||||||
hostnameProfiles = map (n: "hostname.${n}") (
|
|
||||||
builtins.attrNames (baseProject.config.profiles.hostname or { })
|
|
||||||
);
|
|
||||||
userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { }));
|
|
||||||
allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles;
|
|
||||||
in
|
|
||||||
baseProject.config.profiles.${profileName}
|
|
||||||
or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}");
|
|
||||||
|
|
||||||
# Fold over ordered profiles to build final list with extends
|
|
||||||
expandedProfiles = lib.foldl'
|
|
||||||
(
|
|
||||||
acc: profileName:
|
|
||||||
let
|
|
||||||
allProfileNames = resolveProfileExtends profileName [ ];
|
|
||||||
in
|
|
||||||
acc ++ allProfileNames
|
|
||||||
) [ ]
|
|
||||||
orderedProfiles;
|
|
||||||
|
|
||||||
# Map over expanded profiles and apply priorities
|
|
||||||
allPrioritizedModules = lib.imap0
|
|
||||||
(
|
|
||||||
index: profileName:
|
|
||||||
let
|
|
||||||
profilePriority = (lib.modules.defaultOverridePriority - 1) - index;
|
|
||||||
profileConfig = getProfileConfig profileName;
|
|
||||||
|
|
||||||
typeNeedsOverride =
|
|
||||||
type:
|
|
||||||
if type == null then
|
|
||||||
false
|
|
||||||
else
|
|
||||||
let
|
|
||||||
typeName = type.name or type._type or "";
|
|
||||||
|
|
||||||
isLeafType = builtins.elem typeName [
|
|
||||||
"str"
|
|
||||||
"int"
|
|
||||||
"bool"
|
|
||||||
"enum"
|
|
||||||
"path"
|
|
||||||
"package"
|
|
||||||
"float"
|
|
||||||
"anything"
|
|
||||||
];
|
|
||||||
in
|
|
||||||
if isLeafType then
|
|
||||||
true
|
|
||||||
else if typeName == "nullOr" then
|
|
||||||
let
|
|
||||||
innerType =
|
|
||||||
type.elemType
|
|
||||||
or (if type ? nestedTypes && type.nestedTypes ? elemType then type.nestedTypes.elemType else null);
|
|
||||||
in
|
|
||||||
if innerType != null then typeNeedsOverride innerType else false
|
|
||||||
else
|
|
||||||
false;
|
|
||||||
|
|
||||||
pathNeedsOverride =
|
|
||||||
optionPath:
|
|
||||||
let
|
|
||||||
directOption = lib.attrByPath optionPath null baseProject.options;
|
|
||||||
in
|
|
||||||
if directOption != null && lib.isOption directOption then
|
|
||||||
typeNeedsOverride directOption.type
|
|
||||||
else if optionPath != [ ] then
|
|
||||||
let
|
|
||||||
parentPath = lib.init optionPath;
|
|
||||||
parentOption = lib.attrByPath parentPath null baseProject.options;
|
|
||||||
in
|
|
||||||
if parentOption != null && lib.isOption parentOption then
|
|
||||||
let
|
|
||||||
freeformType = parentOption.type.freeformType or parentOption.type.nestedTypes.freeformType or null;
|
|
||||||
elementType =
|
|
||||||
if freeformType ? elemType then
|
|
||||||
freeformType.elemType
|
|
||||||
else if freeformType ? nestedTypes && freeformType.nestedTypes ? elemType then
|
|
||||||
freeformType.nestedTypes.elemType
|
|
||||||
else
|
|
||||||
freeformType;
|
|
||||||
in
|
|
||||||
typeNeedsOverride elementType
|
|
||||||
else
|
|
||||||
false
|
|
||||||
else
|
|
||||||
false;
|
|
||||||
|
|
||||||
applyModuleOverride =
|
|
||||||
config:
|
|
||||||
if builtins.isFunction config then
|
|
||||||
let
|
|
||||||
wrapper = args: applyOverrideRecursive (config args) [ ];
|
|
||||||
in
|
|
||||||
lib.mirrorFunctionArgs config wrapper
|
|
||||||
else
|
|
||||||
applyOverrideRecursive config [ ];
|
|
||||||
|
|
||||||
applyOverrideRecursive =
|
|
||||||
config: optionPath:
|
|
||||||
if lib.isAttrs config && config ? _type then
|
|
||||||
config
|
|
||||||
else if lib.isAttrs config then
|
|
||||||
lib.mapAttrs (name: value: applyOverrideRecursive value (optionPath ++ [ name ])) config
|
|
||||||
else if pathNeedsOverride optionPath then
|
|
||||||
lib.mkOverride profilePriority config
|
|
||||||
else
|
|
||||||
config;
|
|
||||||
|
|
||||||
prioritizedConfig = (
|
|
||||||
profileConfig.module
|
|
||||||
// {
|
|
||||||
imports = lib.map
|
|
||||||
(
|
|
||||||
importItem:
|
|
||||||
importItem
|
|
||||||
// {
|
|
||||||
imports = lib.map (nestedImport: applyModuleOverride nestedImport) (importItem.imports or [ ]);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(profileConfig.module.imports or [ ]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
in
|
|
||||||
prioritizedConfig
|
|
||||||
)
|
|
||||||
expandedProfiles;
|
|
||||||
in
|
|
||||||
if allPrioritizedModules == [ ] then
|
|
||||||
baseProject
|
|
||||||
else
|
|
||||||
baseProject.extendModules { modules = allPrioritizedModules; };
|
|
||||||
|
|
||||||
config = project.config;
|
|
||||||
|
|
||||||
# Per-container scoped re-evaluation that flips `isBuilding` for the
|
|
||||||
# container being built. Selecting one container cannot pollute the
|
|
||||||
# evaluation of any other operation, since each `containerBuilds.<name>`
|
|
||||||
# is its own `extendModules` scope.
|
|
||||||
mkContainerBuilds =
|
|
||||||
evalProject:
|
|
||||||
lib.genAttrs (lib.attrNames evalProject.config.containers) (
|
|
||||||
name:
|
|
||||||
let
|
|
||||||
scoped = evalProject.extendModules {
|
|
||||||
modules = [{
|
|
||||||
container.isBuilding = lib.mkForce true;
|
|
||||||
containers.${name}.isBuilding = lib.mkForce true;
|
|
||||||
}];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
scoped.config.containers.${name}
|
|
||||||
);
|
|
||||||
|
|
||||||
containerBuilds = mkContainerBuilds project;
|
|
||||||
|
|
||||||
# Apply config overlays to pkgs
|
|
||||||
pkgs = pkgsBootstrap.appendOverlays (config.overlays or [ ]);
|
|
||||||
|
|
||||||
options = pkgs.nixosOptionsDoc {
|
|
||||||
options = builtins.removeAttrs project.options [ "_module" ];
|
|
||||||
warningsAreErrors = false;
|
|
||||||
transformOptions =
|
|
||||||
let
|
|
||||||
isDocType =
|
|
||||||
v:
|
|
||||||
builtins.elem v [
|
|
||||||
"literalDocBook"
|
|
||||||
"literalExpression"
|
|
||||||
"literalMD"
|
|
||||||
"mdDoc"
|
|
||||||
];
|
|
||||||
in
|
|
||||||
lib.attrsets.mapAttrs (
|
|
||||||
_: v:
|
|
||||||
if v ? _type && isDocType v._type then
|
|
||||||
v.text
|
|
||||||
else if v ? _type && v._type == "derivation" then
|
|
||||||
v.name
|
|
||||||
else
|
|
||||||
v
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
build =
|
|
||||||
options: config:
|
|
||||||
lib.concatMapAttrs
|
|
||||||
(
|
|
||||||
name: option:
|
|
||||||
if lib.isOption option then
|
|
||||||
let
|
|
||||||
typeName = option.type.name or "";
|
|
||||||
in
|
|
||||||
if
|
|
||||||
builtins.elem typeName [
|
|
||||||
"output"
|
|
||||||
"outputOf"
|
|
||||||
]
|
|
||||||
then
|
|
||||||
{
|
|
||||||
${name} = config.${name};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{ }
|
|
||||||
else if builtins.isAttrs option && !lib.isDerivation option then
|
|
||||||
let
|
|
||||||
v = build option config.${name};
|
|
||||||
in
|
|
||||||
if v != { } then { ${name} = v; } else { }
|
|
||||||
else
|
|
||||||
{ }
|
|
||||||
)
|
|
||||||
options;
|
|
||||||
|
|
||||||
# Helper to evaluate devenv for a specific system (for cross-compilation, e.g. macOS building Linux containers)
|
|
||||||
evalForSystem =
|
|
||||||
evalSystem:
|
|
||||||
let
|
|
||||||
evalPkgs = mkPkgsForSystem evalSystem;
|
|
||||||
evalProject = lib.evalModules {
|
|
||||||
specialArgs = inputs // {
|
|
||||||
inherit inputs secretspec primops;
|
|
||||||
};
|
|
||||||
modules = mkCommonModules evalPkgs;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = evalProject.config;
|
|
||||||
containerBuilds = mkContainerBuilds evalProject;
|
|
||||||
};
|
|
||||||
|
|
||||||
# All supported systems for cross-compilation (lazily evaluated)
|
|
||||||
allSystems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Generate perSystem entries for all systems (only evaluated when accessed)
|
|
||||||
perSystemConfigs = lib.genAttrs allSystems (
|
|
||||||
perSystem:
|
|
||||||
if perSystem == targetSystem then
|
|
||||||
{ inherit config containerBuilds; }
|
|
||||||
else
|
|
||||||
evalForSystem perSystem
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
pkgs
|
|
||||||
config
|
|
||||||
options
|
|
||||||
project
|
|
||||||
;
|
|
||||||
bash = pkgs.bash;
|
|
||||||
shell = config.shell;
|
|
||||||
optionsJSON = options.optionsJSON;
|
|
||||||
info = config.info;
|
|
||||||
ci = config.ciDerivation;
|
|
||||||
build = build project.options config;
|
|
||||||
devenv = {
|
|
||||||
# Backwards compatibility: wrap config in devenv attribute for code expecting devenv.config.*
|
|
||||||
inherit config containerBuilds;
|
|
||||||
# perSystem structure for cross-compilation (e.g. macOS building Linux containers)
|
|
||||||
perSystem = perSystemConfigs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Simplified devenv evaluation for inputs
|
|
||||||
# This is a lightweight version suitable for evaluating an input's devenv.nix
|
|
||||||
mkDevenvForInput =
|
|
||||||
{
|
|
||||||
# The input to evaluate (must have outPath and sourceInfo)
|
|
||||||
input
|
|
||||||
, # All resolved inputs (for specialArgs)
|
|
||||||
allInputs
|
|
||||||
, # System to evaluate for
|
|
||||||
system ? builtins.currentSystem
|
|
||||||
, # Nixpkgs to use (defaults to allInputs.nixpkgs)
|
|
||||||
nixpkgs ? allInputs.nixpkgs or (throw "nixpkgs input required")
|
|
||||||
, # Devenv modules (defaults to allInputs.devenv)
|
|
||||||
devenv ? allInputs.devenv or (throw "devenv input required")
|
|
||||||
,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
devenvPath = input.outPath + "/devenv.nix";
|
|
||||||
hasDevenv = builtins.pathExists devenvPath;
|
|
||||||
in
|
|
||||||
if !hasDevenv then
|
|
||||||
throw ''
|
|
||||||
Input does not have a devenv.nix file.
|
|
||||||
Expected file at: ${devenvPath}
|
|
||||||
|
|
||||||
To use this input's devenv configuration, the input must provide a devenv.nix file.
|
|
||||||
''
|
|
||||||
else
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
config = { };
|
|
||||||
};
|
|
||||||
lib = pkgs.lib;
|
|
||||||
|
|
||||||
project = lib.evalModules {
|
|
||||||
specialArgs = allInputs // {
|
|
||||||
inputs = allInputs;
|
|
||||||
secretspec = null;
|
|
||||||
};
|
|
||||||
modules = [
|
|
||||||
(
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
_module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
(devenv.outPath + "/src/modules/top-level.nix")
|
|
||||||
(import devenvPath)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit pkgs;
|
|
||||||
config = project.config;
|
|
||||||
options = project.options;
|
|
||||||
inherit project;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
args@{ system
|
|
||||||
, # The project root (location of devenv.nix)
|
|
||||||
devenv_root
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
inherit
|
|
||||||
(import ./resolve-lock.nix {
|
|
||||||
src = devenv_root;
|
|
||||||
inherit system;
|
|
||||||
})
|
|
||||||
inputs
|
|
||||||
;
|
|
||||||
|
|
||||||
bootstrapLib = import ./bootstrapLib.nix { inherit inputs; };
|
|
||||||
in
|
|
||||||
|
|
||||||
bootstrapLib.mkDevenvForSystem args
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# Adapted from https://git.lix.systems/lix-project/flake-compat/src/branch/main/default.nix
|
|
||||||
{ src
|
|
||||||
, system ? builtins.currentSystem or "unknown-system"
|
|
||||||
,
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
lockFilePath = src + "/devenv.lock";
|
|
||||||
|
|
||||||
lockFile = builtins.fromJSON (builtins.readFile lockFilePath);
|
|
||||||
|
|
||||||
rootSrc = {
|
|
||||||
lastModified = 0;
|
|
||||||
lastModifiedDate = formatSecondsSinceEpoch 0;
|
|
||||||
# *hacker voice*: it's definitely a store path, I promise (actually a
|
|
||||||
# nixlang path value, likely not pointing at the store).
|
|
||||||
outPath = src;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Format number of seconds in the Unix epoch as %Y%m%d%H%M%S.
|
|
||||||
formatSecondsSinceEpoch =
|
|
||||||
t:
|
|
||||||
let
|
|
||||||
rem = x: y: x - x / y * y;
|
|
||||||
days = t / 86400;
|
|
||||||
secondsInDay = rem t 86400;
|
|
||||||
hours = secondsInDay / 3600;
|
|
||||||
minutes = (rem secondsInDay 3600) / 60;
|
|
||||||
seconds = rem t 60;
|
|
||||||
|
|
||||||
# Courtesy of https://stackoverflow.com/a/32158604.
|
|
||||||
z = days + 719468;
|
|
||||||
era = (if z >= 0 then z else z - 146096) / 146097;
|
|
||||||
doe = z - era * 146097;
|
|
||||||
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
||||||
y = yoe + era * 400;
|
|
||||||
doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
mp = (5 * doy + 2) / 153;
|
|
||||||
d = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
m = mp + (if mp < 10 then 3 else -9);
|
|
||||||
y' = y + (if m <= 2 then 1 else 0);
|
|
||||||
|
|
||||||
pad = s: if builtins.stringLength s < 2 then "0" + s else s;
|
|
||||||
in
|
|
||||||
"${toString y'}${pad (toString m)}${pad (toString d)}${pad (toString hours)}${pad (toString minutes)}${pad (toString seconds)}";
|
|
||||||
|
|
||||||
allNodes = builtins.mapAttrs
|
|
||||||
(
|
|
||||||
key: node:
|
|
||||||
let
|
|
||||||
sourceInfo =
|
|
||||||
if key == lockFile.root then
|
|
||||||
rootSrc
|
|
||||||
# Path inputs pointing to project root (path = ".") should use rootSrc
|
|
||||||
# to avoid fetchTree hashing the entire project directory
|
|
||||||
else if node.locked.type or null == "path" && node.locked.path or null == "." then
|
|
||||||
rootSrc
|
|
||||||
else
|
|
||||||
let
|
|
||||||
locked = node.locked;
|
|
||||||
isRelativePath = p: p != null && (builtins.substring 0 2 p == "./" || builtins.substring 0 3 p == "../");
|
|
||||||
# Resolve relative paths against src
|
|
||||||
resolvedLocked = locked
|
|
||||||
// (if locked.type or null == "path" && isRelativePath (locked.path or null)
|
|
||||||
then { path = toString src + "/${locked.path}"; }
|
|
||||||
else { })
|
|
||||||
// (if locked.type or null == "git" && isRelativePath (locked.url or null)
|
|
||||||
then { url = toString src + "/${locked.url}"; }
|
|
||||||
else { });
|
|
||||||
in
|
|
||||||
builtins.fetchTree (node.info or { } // removeAttrs resolvedLocked [ "dir" ]);
|
|
||||||
|
|
||||||
subdir = if key == lockFile.root then "" else node.locked.dir or "";
|
|
||||||
|
|
||||||
outPath = sourceInfo + ((if subdir == "" then "" else "/") + subdir);
|
|
||||||
|
|
||||||
# Resolve a input spec into a node name. An input spec is
|
|
||||||
# either a node name, or a 'follows' path from the root
|
|
||||||
# node.
|
|
||||||
resolveInput =
|
|
||||||
inputSpec: if builtins.isList inputSpec then getInputByPath lockFile.root inputSpec else inputSpec;
|
|
||||||
|
|
||||||
# Follow an input path (e.g. ["dwarffs" "nixpkgs"]) from the
|
|
||||||
# root node, returning the final node.
|
|
||||||
getInputByPath =
|
|
||||||
nodeName: path:
|
|
||||||
if path == [ ] then
|
|
||||||
nodeName
|
|
||||||
else
|
|
||||||
getInputByPath
|
|
||||||
# Since this could be a 'follows' input, call resolveInput.
|
|
||||||
(resolveInput lockFile.nodes.${nodeName}.inputs.${builtins.head path})
|
|
||||||
(builtins.tail path);
|
|
||||||
|
|
||||||
inputs = builtins.mapAttrs (inputName: inputSpec: allNodes.${resolveInput inputSpec}) (
|
|
||||||
node.inputs or { }
|
|
||||||
);
|
|
||||||
|
|
||||||
# Only import flake.nix for non-root nodes (root doesn't need it)
|
|
||||||
flake = if key == lockFile.root then null else import (outPath + "/flake.nix");
|
|
||||||
|
|
||||||
outputs = if key == lockFile.root then { } else flake.outputs (inputs // { self = result; });
|
|
||||||
|
|
||||||
# Lazy devenv evaluation for this input
|
|
||||||
devenvEval =
|
|
||||||
let
|
|
||||||
bootstrapLib = import ./bootstrapLib.nix { inputs = inputs; };
|
|
||||||
in
|
|
||||||
bootstrapLib.mkDevenvForInput {
|
|
||||||
input = { inherit outPath sourceInfo; };
|
|
||||||
allInputs = inputs;
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
|
|
||||||
result =
|
|
||||||
outputs
|
|
||||||
// sourceInfo
|
|
||||||
// {
|
|
||||||
inherit outPath;
|
|
||||||
inherit inputs;
|
|
||||||
inherit outputs;
|
|
||||||
inherit sourceInfo;
|
|
||||||
_type = "flake";
|
|
||||||
devenv = devenvEval;
|
|
||||||
};
|
|
||||||
|
|
||||||
nonFlakeResult = sourceInfo // {
|
|
||||||
inherit outPath;
|
|
||||||
inherit inputs;
|
|
||||||
inherit sourceInfo;
|
|
||||||
_type = "flake";
|
|
||||||
devenv = devenvEval;
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
if node.flake or true && key != lockFile.root then
|
|
||||||
assert builtins.isFunction flake.outputs;
|
|
||||||
result
|
|
||||||
else
|
|
||||||
nonFlakeResult
|
|
||||||
)
|
|
||||||
lockFile.nodes;
|
|
||||||
|
|
||||||
result =
|
|
||||||
if !(builtins.pathExists lockFilePath) then
|
|
||||||
throw "${lockFilePath} does not exist"
|
|
||||||
else if lockFile.version >= 5 && lockFile.version <= 7 then
|
|
||||||
allNodes.${lockFile.root}
|
|
||||||
else
|
|
||||||
throw "lock file '${lockFilePath}' has unsupported version ${toString lockFile.version}";
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inputs = result.inputs or { } // {
|
|
||||||
self = result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/9dswnx96sj7qpqvah77lx8g25hsl1z1x-devenv-shell
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/gj888l55lxj0brzhkjrdcald7zw7pskj-tasks.json
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/home/user01/Projects/score-system/.devenv/bootstrap/bootstrapLib.nix
|
|
||||||
/home/user01/Projects/score-system/.devenv/bootstrap/default.nix
|
|
||||||
/home/user01/Projects/score-system/.devenv/bootstrap/resolve-lock.nix
|
|
||||||
/home/user01/Projects/score-system/devenv.local.nix
|
|
||||||
/home/user01/Projects/score-system/devenv.lock
|
|
||||||
/home/user01/Projects/score-system/devenv.nix
|
|
||||||
/home/user01/Projects/score-system/devenv.yaml
|
|
||||||
/home/user01/.config/nixpkgs/overlays
|
|
||||||
/home/user01/.config/nixpkgs/overlays.nix
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
let
|
|
||||||
cfg = {};
|
|
||||||
getName = pkg: (builtins.parseDrvName (pkg.name or pkg.pname or "")).name;
|
|
||||||
in cfg // {
|
|
||||||
allowUnfreePredicate =
|
|
||||||
if cfg.allowUnfree or false then
|
|
||||||
(_: true)
|
|
||||||
else if (cfg.permittedUnfreePackages or []) != [] then
|
|
||||||
(pkg: builtins.elem (getName pkg) (cfg.permittedUnfreePackages or []))
|
|
||||||
else
|
|
||||||
(_: false);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/z7jz33yvsqvfv3qxpy2r9mp3mh0ngcvg-devenv-profile
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/run/user/1000/devenv-3f21a4e
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2250
.devenv/shell-env.sh
2250
.devenv/shell-env.sh
File diff suppressed because it is too large
Load Diff
@@ -1,163 +0,0 @@
|
|||||||
# Disable history during init so devenv internal commands don't pollute history.
|
|
||||||
set +o history
|
|
||||||
|
|
||||||
# Environment diff helpers (always defined for tracking)
|
|
||||||
|
|
||||||
# Environment diff helpers (inspired by direnv)
|
|
||||||
# Diff is stored in _DEVENV_DIFF env var (not a file) so each shell has its own state
|
|
||||||
# Uses gzip+base64 encoding for compact storage
|
|
||||||
|
|
||||||
# Variables to ignore in diff (shell internals that change dynamically)
|
|
||||||
__devenv_ignored_var() {
|
|
||||||
case "$1" in
|
|
||||||
_*|PWD|OLDPWD|SHLVL|SHELL|SHELLOPTS|BASHOPTS|BASH_*|HISTCMD|HISTFILE)
|
|
||||||
return 0 ;;
|
|
||||||
PS1|PS2|PS3|PS4|PROMPT|PROMPT_COMMAND|PROMPT_DIRTRIM)
|
|
||||||
return 0 ;;
|
|
||||||
COMP_*|READLINE_*|MAILCHECK|COLUMNS|LINES|RANDOM|SECONDS|LINENO|EPOCHSECONDS|EPOCHREALTIME|SRANDOM)
|
|
||||||
return 0 ;;
|
|
||||||
STARSHIP_*|__fish*|DIRENV_*|nix_saved_*)
|
|
||||||
return 0 ;;
|
|
||||||
*)
|
|
||||||
return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_capture_env() {
|
|
||||||
# Capture exported variables using declare -p for proper escaping
|
|
||||||
declare -p -x 2>/dev/null | LC_ALL=C sort
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_serialize_diff() {
|
|
||||||
# Serialize diff (stdin) to base64-encoded gzip
|
|
||||||
gzip -c | base64 -w0
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_deserialize_diff() {
|
|
||||||
# Deserialize diff from base64-encoded gzip to stdout
|
|
||||||
echo "$1" | base64 -d | gzip -d 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_compute_diff() {
|
|
||||||
# Compare before ($1) and current env, return diff via _DEVENV_DIFF env var
|
|
||||||
local before_file="$1"
|
|
||||||
|
|
||||||
# Create temp files
|
|
||||||
local after_file diff_content
|
|
||||||
after_file=$(mktemp)
|
|
||||||
diff_content=$(mktemp)
|
|
||||||
__devenv_capture_env > "$after_file"
|
|
||||||
|
|
||||||
# Build associative arrays for before/after
|
|
||||||
local -A before_vars after_vars
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ "$line" != declare\ -x\ * ]] && continue
|
|
||||||
local vardef="${line#declare -x }"
|
|
||||||
local var="${vardef%%=*}"
|
|
||||||
[[ -z "$var" ]] && continue
|
|
||||||
__devenv_ignored_var "$var" && continue
|
|
||||||
before_vars["$var"]="$line"
|
|
||||||
done < "$before_file"
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ "$line" != declare\ -x\ * ]] && continue
|
|
||||||
local vardef="${line#declare -x }"
|
|
||||||
local var="${vardef%%=*}"
|
|
||||||
[[ -z "$var" ]] && continue
|
|
||||||
__devenv_ignored_var "$var" && continue
|
|
||||||
after_vars["$var"]="$line"
|
|
||||||
done < "$after_file"
|
|
||||||
|
|
||||||
# Find PREV entries (vars that were modified or removed)
|
|
||||||
for var in "${!before_vars[@]}"; do
|
|
||||||
if [[ "${after_vars[$var]}" != "${before_vars[$var]}" ]]; then
|
|
||||||
echo "P:${before_vars[$var]}" >> "$diff_content"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Find NEXT entries (vars that were added or modified)
|
|
||||||
for var in "${!after_vars[@]}"; do
|
|
||||||
if [[ -z "${before_vars[$var]+x}" ]]; then
|
|
||||||
echo "N:$var" >> "$diff_content"
|
|
||||||
elif [[ "${after_vars[$var]}" != "${before_vars[$var]}" ]]; then
|
|
||||||
echo "N:$var" >> "$diff_content"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Serialize and store in env var
|
|
||||||
_DEVENV_DIFF=$(__devenv_serialize_diff < "$diff_content")
|
|
||||||
export _DEVENV_DIFF
|
|
||||||
|
|
||||||
rm -f "$after_file" "$diff_content"
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_apply_reverse_diff() {
|
|
||||||
# Reverse the diff: restore PREV values, unset NEXT-only vars
|
|
||||||
[[ -z "$_DEVENV_DIFF" ]] && return
|
|
||||||
|
|
||||||
local -A prev_vars
|
|
||||||
local diff_content
|
|
||||||
diff_content=$(__devenv_deserialize_diff "$_DEVENV_DIFF")
|
|
||||||
|
|
||||||
# First pass: collect and restore PREV declarations
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" == P:declare\ * ]]; then
|
|
||||||
local decl="${line#P:}"
|
|
||||||
local var="${decl#declare -x }"
|
|
||||||
var="${var%%=*}"
|
|
||||||
prev_vars["$var"]=1
|
|
||||||
# Use export instead of evaluating the declare statement directly,
|
|
||||||
# because declare -x inside a function creates a local variable
|
|
||||||
# in bash 5.0+.
|
|
||||||
eval "export ${decl#declare -x }" 2>/dev/null
|
|
||||||
fi
|
|
||||||
done <<< "$diff_content"
|
|
||||||
|
|
||||||
# Second pass: unset NEXT vars that were not in PREV (added vars)
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" == N:* ]]; then
|
|
||||||
local var="${line#N:}"
|
|
||||||
if [[ -z "${prev_vars[$var]+x}" ]]; then
|
|
||||||
unset "$var"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$diff_content"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Capture environment BEFORE sourcing devenv (for diff tracking)
|
|
||||||
_devenv_before_file=$(mktemp)
|
|
||||||
__devenv_capture_env > "$_devenv_before_file"
|
|
||||||
|
|
||||||
# Source the devenv environment
|
|
||||||
source "/home/user01/Projects/score-system/.devenv/shell-env.sh"
|
|
||||||
|
|
||||||
# Compute and store the initial diff in _DEVENV_DIFF env var
|
|
||||||
__devenv_compute_diff "$_devenv_before_file"
|
|
||||||
rm -f "$_devenv_before_file"
|
|
||||||
unset _devenv_before_file
|
|
||||||
|
|
||||||
# Save PATH before zsh init potentially modifies it
|
|
||||||
export _DEVENV_PATH="$PATH"
|
|
||||||
|
|
||||||
# Save original ZDOTDIR so zsh init can restore it
|
|
||||||
if [ -n "$ZDOTDIR" ]; then
|
|
||||||
export _DEVENV_REAL_ZDOTDIR="$ZDOTDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Point ZDOTDIR to our init directory containing our .zshrc
|
|
||||||
export ZDOTDIR="/home/user01/Projects/score-system/.devenv/zsh"
|
|
||||||
|
|
||||||
# Re-enable history before exec
|
|
||||||
set -o history
|
|
||||||
|
|
||||||
# Exec into zsh (resolve via PATH if not absolute, since the devenv
|
|
||||||
# environment may have added it after this process started)
|
|
||||||
if [ ! -x "/run/current-system/sw/bin/zsh" ] && ! command -v "/run/current-system/sw/bin/zsh" >/dev/null 2>&1; then
|
|
||||||
echo "devenv: error: shell '/run/current-system/sw/bin/zsh' not found" >&2
|
|
||||||
echo "devenv: add zsh to your devenv.nix packages or set SHELL to an absolute path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec "/run/current-system/sw/bin/zsh" -i
|
|
||||||
echo "devenv: error: failed to exec into /run/current-system/sw/bin/zsh" >&2
|
|
||||||
exit 1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"managedFiles":[]}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
devenv:container:copy
|
|
||||||
devenv:enterShell
|
|
||||||
devenv:enterTest
|
|
||||||
devenv:files
|
|
||||||
devenv:files:cleanup
|
|
||||||
devenv:processes:server
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,6 +0,0 @@
|
|||||||
# devenv zsh .zshenv - runs before /etc/zshrc.
|
|
||||||
# Prepend devenv profile site-functions so the system compinit (often
|
|
||||||
# called from /etc/zshrc on nix-darwin, Debian, etc.) picks them up.
|
|
||||||
if [ -n "$DEVENV_PROFILE" ] && [ -d "$DEVENV_PROFILE/share/zsh/site-functions" ]; then
|
|
||||||
fpath=("$DEVENV_PROFILE/share/zsh/site-functions" $fpath)
|
|
||||||
fi
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
# devenv zsh init - restore ZDOTDIR and source user's .zshrc
|
|
||||||
|
|
||||||
if [ -n "$_DEVENV_REAL_ZDOTDIR" ]; then
|
|
||||||
ZDOTDIR="$_DEVENV_REAL_ZDOTDIR"
|
|
||||||
unset _DEVENV_REAL_ZDOTDIR
|
|
||||||
[ -f "$ZDOTDIR/.zshenv" ] && source "$ZDOTDIR/.zshenv"
|
|
||||||
[ -f "$ZDOTDIR/.zshrc" ] && source "$ZDOTDIR/.zshrc"
|
|
||||||
else
|
|
||||||
unset ZDOTDIR
|
|
||||||
[ -f "$HOME/.zshenv" ] && source "$HOME/.zshenv"
|
|
||||||
[ -f "$HOME/.zshrc" ] && source "$HOME/.zshrc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore devenv PATH after user's .zshrc may have modified it
|
|
||||||
export PATH="$_DEVENV_PATH"
|
|
||||||
|
|
||||||
# Set devenv prompt prefix
|
|
||||||
PROMPT="(devenv) ${PROMPT}"
|
|
||||||
|
|
||||||
# Hot-reload hook
|
|
||||||
|
|
||||||
autoload -Uz add-zsh-hook
|
|
||||||
|
|
||||||
__devenv_reload_apply() {
|
|
||||||
# Source new environment if a reload is pending
|
|
||||||
if [ -f "/tmp/devenv-reload-274471.sh" ]; then
|
|
||||||
# Shell out to bash to handle the env diff (bash syntax)
|
|
||||||
local reload_output
|
|
||||||
reload_output=$(bash -c '
|
|
||||||
|
|
||||||
# Environment diff helpers (inspired by direnv)
|
|
||||||
# Diff is stored in _DEVENV_DIFF env var (not a file) so each shell has its own state
|
|
||||||
# Uses gzip+base64 encoding for compact storage
|
|
||||||
|
|
||||||
# Variables to ignore in diff (shell internals that change dynamically)
|
|
||||||
__devenv_ignored_var() {
|
|
||||||
case "$1" in
|
|
||||||
_*|PWD|OLDPWD|SHLVL|SHELL|SHELLOPTS|BASHOPTS|BASH_*|HISTCMD|HISTFILE)
|
|
||||||
return 0 ;;
|
|
||||||
PS1|PS2|PS3|PS4|PROMPT|PROMPT_COMMAND|PROMPT_DIRTRIM)
|
|
||||||
return 0 ;;
|
|
||||||
COMP_*|READLINE_*|MAILCHECK|COLUMNS|LINES|RANDOM|SECONDS|LINENO|EPOCHSECONDS|EPOCHREALTIME|SRANDOM)
|
|
||||||
return 0 ;;
|
|
||||||
STARSHIP_*|__fish*|DIRENV_*|nix_saved_*)
|
|
||||||
return 0 ;;
|
|
||||||
*)
|
|
||||||
return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_capture_env() {
|
|
||||||
# Capture exported variables using declare -p for proper escaping
|
|
||||||
declare -p -x 2>/dev/null | LC_ALL=C sort
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_serialize_diff() {
|
|
||||||
# Serialize diff (stdin) to base64-encoded gzip
|
|
||||||
gzip -c | base64 -w0
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_deserialize_diff() {
|
|
||||||
# Deserialize diff from base64-encoded gzip to stdout
|
|
||||||
echo "$1" | base64 -d | gzip -d 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_compute_diff() {
|
|
||||||
# Compare before ($1) and current env, return diff via _DEVENV_DIFF env var
|
|
||||||
local before_file="$1"
|
|
||||||
|
|
||||||
# Create temp files
|
|
||||||
local after_file diff_content
|
|
||||||
after_file=$(mktemp)
|
|
||||||
diff_content=$(mktemp)
|
|
||||||
__devenv_capture_env > "$after_file"
|
|
||||||
|
|
||||||
# Build associative arrays for before/after
|
|
||||||
local -A before_vars after_vars
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ "$line" != declare\ -x\ * ]] && continue
|
|
||||||
local vardef="${line#declare -x }"
|
|
||||||
local var="${vardef%%=*}"
|
|
||||||
[[ -z "$var" ]] && continue
|
|
||||||
__devenv_ignored_var "$var" && continue
|
|
||||||
before_vars["$var"]="$line"
|
|
||||||
done < "$before_file"
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ "$line" != declare\ -x\ * ]] && continue
|
|
||||||
local vardef="${line#declare -x }"
|
|
||||||
local var="${vardef%%=*}"
|
|
||||||
[[ -z "$var" ]] && continue
|
|
||||||
__devenv_ignored_var "$var" && continue
|
|
||||||
after_vars["$var"]="$line"
|
|
||||||
done < "$after_file"
|
|
||||||
|
|
||||||
# Find PREV entries (vars that were modified or removed)
|
|
||||||
for var in "${!before_vars[@]}"; do
|
|
||||||
if [[ "${after_vars[$var]}" != "${before_vars[$var]}" ]]; then
|
|
||||||
echo "P:${before_vars[$var]}" >> "$diff_content"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Find NEXT entries (vars that were added or modified)
|
|
||||||
for var in "${!after_vars[@]}"; do
|
|
||||||
if [[ -z "${before_vars[$var]+x}" ]]; then
|
|
||||||
echo "N:$var" >> "$diff_content"
|
|
||||||
elif [[ "${after_vars[$var]}" != "${before_vars[$var]}" ]]; then
|
|
||||||
echo "N:$var" >> "$diff_content"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Serialize and store in env var
|
|
||||||
_DEVENV_DIFF=$(__devenv_serialize_diff < "$diff_content")
|
|
||||||
export _DEVENV_DIFF
|
|
||||||
|
|
||||||
rm -f "$after_file" "$diff_content"
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_apply_reverse_diff() {
|
|
||||||
# Reverse the diff: restore PREV values, unset NEXT-only vars
|
|
||||||
[[ -z "$_DEVENV_DIFF" ]] && return
|
|
||||||
|
|
||||||
local -A prev_vars
|
|
||||||
local diff_content
|
|
||||||
diff_content=$(__devenv_deserialize_diff "$_DEVENV_DIFF")
|
|
||||||
|
|
||||||
# First pass: collect and restore PREV declarations
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" == P:declare\ * ]]; then
|
|
||||||
local decl="${line#P:}"
|
|
||||||
local var="${decl#declare -x }"
|
|
||||||
var="${var%%=*}"
|
|
||||||
prev_vars["$var"]=1
|
|
||||||
# Use export instead of evaluating the declare statement directly,
|
|
||||||
# because declare -x inside a function creates a local variable
|
|
||||||
# in bash 5.0+.
|
|
||||||
eval "export ${decl#declare -x }" 2>/dev/null
|
|
||||||
fi
|
|
||||||
done <<< "$diff_content"
|
|
||||||
|
|
||||||
# Second pass: unset NEXT vars that were not in PREV (added vars)
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" == N:* ]]; then
|
|
||||||
local var="${line#N:}"
|
|
||||||
if [[ -z "${prev_vars[$var]+x}" ]]; then
|
|
||||||
unset "$var"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$diff_content"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Reverse previous diff
|
|
||||||
__devenv_apply_reverse_diff
|
|
||||||
|
|
||||||
# Capture env before sourcing new devenv
|
|
||||||
_before=$(mktemp)
|
|
||||||
__devenv_capture_env > "$_before"
|
|
||||||
|
|
||||||
# Source new devenv environment
|
|
||||||
source "/tmp/devenv-reload-274471.sh"
|
|
||||||
rm -f "/tmp/devenv-reload-274471.sh"
|
|
||||||
|
|
||||||
# Compute new diff
|
|
||||||
__devenv_compute_diff "$_before"
|
|
||||||
rm -f "$_before"
|
|
||||||
|
|
||||||
# Output current environment for the calling shell to parse
|
|
||||||
export -p
|
|
||||||
' 2>/dev/null)
|
|
||||||
|
|
||||||
# Apply the environment changes
|
|
||||||
if [ -n "$reload_output" ]; then
|
|
||||||
eval "$reload_output"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update saved PATH
|
|
||||||
_DEVENV_PATH="$PATH"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_restore_path() {
|
|
||||||
# Restore devenv PATH (in case direnv or other tools modified it)
|
|
||||||
export PATH="$_DEVENV_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
__devenv_precmd_hook() {
|
|
||||||
__devenv_reload_apply
|
|
||||||
__devenv_restore_path
|
|
||||||
}
|
|
||||||
add-zsh-hook precmd __devenv_precmd_hook
|
|
||||||
|
|
||||||
# Keybinding for manual reload
|
|
||||||
__devenv_reload_widget() {
|
|
||||||
__devenv_reload_apply
|
|
||||||
zle reset-prompt
|
|
||||||
}
|
|
||||||
zle -N __devenv_reload_widget
|
|
||||||
bindkey "${DEVENV_RELOAD_KEYBIND:-\\e\\C-r}" __devenv_reload_widget
|
|
||||||
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ vite.config.ts.timestamp-*
|
|||||||
.session
|
.session
|
||||||
local.db
|
local.db
|
||||||
/.devenv
|
/.devenv
|
||||||
|
/.devenv
|
||||||
|
|||||||
15
;
15
;
@@ -1,15 +0,0 @@
|
|||||||
{ pkgs, config, ... }: {
|
|
||||||
languages.typescript.enable = true;
|
|
||||||
dotenv.disableHint = true;
|
|
||||||
packages = with pkgs; [
|
|
||||||
bun
|
|
||||||
eslint_d
|
|
||||||
];
|
|
||||||
env.DEVSHELL_NAME = " devenv/#fab387| Bun/yellow";
|
|
||||||
processes = {
|
|
||||||
server = {
|
|
||||||
ports.http.allocate = 5173;
|
|
||||||
exec = "bun run dev";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
36
TODO.md
Normal file
36
TODO.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# UI - Medium priority
|
||||||
|
player view - all registered events and scores for those events
|
||||||
|
team view - all registered players and all points gained from them - Do i need this?
|
||||||
|
|
||||||
|
# Login stuff - HIGH PRIORITY
|
||||||
|
disableable register button on the login page for end users
|
||||||
|
make a way to toggle on and off the register page
|
||||||
|
protect the register endpoint?? (low prio)
|
||||||
|
|
||||||
|
# Ledger - HIGH PRIORITY
|
||||||
|
ledger view
|
||||||
|
inserting manually into ledger
|
||||||
|
|
||||||
|
# Database - HIGH PRIORITY
|
||||||
|
make events score based off of highest score instead of avg
|
||||||
|
load results from the excel sheet i made
|
||||||
|
export whole contest results to csv
|
||||||
|
|
||||||
|
# OPTIONAL (low priority)
|
||||||
|
chat thing
|
||||||
|
protect all the endpoints?????? (pain in the ass)
|
||||||
|
|
||||||
|
> probably need just in case, seems possible with one endpoint/interface
|
||||||
|
## Non-seeded data
|
||||||
|
manual registering of teams
|
||||||
|
live editing of event/score presets
|
||||||
|
manual registering of events
|
||||||
|
manual registering of players
|
||||||
|
result amendments
|
||||||
|
|
||||||
|
## Easy shit
|
||||||
|
animations for all leaderboards
|
||||||
|
Ongoing since... or ongoing for... on the main page
|
||||||
|
displaying averages of loaded scores when there's more than one (easy, low prio)
|
||||||
|
fix the layout on event view to match the homepage view
|
||||||
|
fix the marquee animation thing and make it global
|
||||||
@@ -20,8 +20,6 @@ export default defineConfig(
|
|||||||
{
|
{
|
||||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||||
rules: {
|
rules: {
|
||||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
|
||||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
|
||||||
'no-undef': 'off'
|
'no-undef': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,8 +35,6 @@ export default defineConfig(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Override or add rule settings here, such as:
|
|
||||||
// 'svelte/button-has-type': 'error'
|
|
||||||
rules: {}
|
rules: {}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function readCSV(filename: string): Record<string, any>[] {
|
|||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('Resetting database...');
|
console.log('Resetting database...');
|
||||||
|
|
||||||
// Disable foreign keys globally during setup to prevent structural mismatches
|
// Temporarily disable FK checks during reset
|
||||||
await client.execute('PRAGMA foreign_keys = OFF');
|
await client.execute('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
await db.delete(schema.scoreLedger);
|
await db.delete(schema.scoreLedger);
|
||||||
@@ -53,30 +53,35 @@ async function seed() {
|
|||||||
let passwordHash = await Bun.password.hash('password');
|
let passwordHash = await Bun.password.hash('password');
|
||||||
await db
|
await db
|
||||||
.insert(schema.scorers)
|
.insert(schema.scorers)
|
||||||
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash });
|
.values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
username: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
passwordHash: passwordHash
|
||||||
|
});
|
||||||
|
|
||||||
// --- 1. Teams ---
|
// Seed teams
|
||||||
const teamsCSV = readCSV('teams.csv');
|
const teamsCSV = readCSV('teams.csv');
|
||||||
for (const row of teamsCSV) {
|
for (const row of teamsCSV) {
|
||||||
await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
|
await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
|
||||||
console.log(` → Team: ${row.team_name} (${row.color})`);
|
console.log(` → Team: ${row.team_name} (${row.color})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Divisions ---
|
// Seed divisions
|
||||||
const divisionsCSV = readCSV('divisions.csv');
|
const divisionsCSV = readCSV('divisions.csv');
|
||||||
for (const row of divisionsCSV) {
|
for (const row of divisionsCSV) {
|
||||||
await db.insert(schema.divisions).values({ name: row.div_name });
|
await db.insert(schema.divisions).values({ name: row.div_name });
|
||||||
console.log(` → Division: ${row.div_name}`);
|
console.log(` → Division: ${row.div_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2.5 Brackets (Added Section) ---
|
// Seed brackets
|
||||||
const bracketsCSV = readCSV('brackets.csv');
|
const bracketsCSV = readCSV('brackets.csv');
|
||||||
for (const row of bracketsCSV) {
|
for (const row of bracketsCSV) {
|
||||||
await db.insert(schema.brackets).values({ name: row.bracket_name });
|
await db.insert(schema.brackets).values({ name: row.bracket_name });
|
||||||
console.log(` → Bracket: ${row.bracket_name}`);
|
console.log(` → Bracket: ${row.bracket_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2.75 resultPresets ---
|
// Seed result presets
|
||||||
const resultPresetsCSV = readCSV('resultPresets.csv');
|
const resultPresetsCSV = readCSV('resultPresets.csv');
|
||||||
for (const row of resultPresetsCSV) {
|
for (const row of resultPresetsCSV) {
|
||||||
await db.insert(schema.resultPresets).values({
|
await db.insert(schema.resultPresets).values({
|
||||||
@@ -91,7 +96,7 @@ async function seed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Scoring Presets ---
|
// Seed scoring presets
|
||||||
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
||||||
for (const row of scoringPresetsCSV) {
|
for (const row of scoringPresetsCSV) {
|
||||||
await db.insert(schema.scoringPresets).values({
|
await db.insert(schema.scoringPresets).values({
|
||||||
@@ -102,19 +107,19 @@ async function seed() {
|
|||||||
console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`);
|
console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps for dynamic relational lookups
|
// Build lookup maps for relational seeding
|
||||||
const dbTeams = await db.select().from(schema.teams);
|
const dbTeams = await db.select().from(schema.teams);
|
||||||
const dbDivisions = await db.select().from(schema.divisions);
|
const dbDivisions = await db.select().from(schema.divisions);
|
||||||
const dbResults = await db.select().from(schema.resultPresets);
|
const dbResults = await db.select().from(schema.resultPresets);
|
||||||
const dbBrackets = await db.select().from(schema.brackets); // Look up newly seeded brackets
|
const dbBrackets = await db.select().from(schema.brackets);
|
||||||
|
|
||||||
const teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
|
const teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
|
||||||
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
|
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
|
||||||
const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name]));
|
const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name]));
|
||||||
const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id])); // Map names to IDs
|
const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id]));
|
||||||
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id])); // Map names to IDs
|
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id]));
|
||||||
|
|
||||||
// --- 4. Players ---
|
// Seed players
|
||||||
const playersCSV = readCSV('players.csv');
|
const playersCSV = readCSV('players.csv');
|
||||||
for (const row of playersCSV) {
|
for (const row of playersCSV) {
|
||||||
const teamId = teamMap.get(row.team);
|
const teamId = teamMap.get(row.team);
|
||||||
@@ -132,7 +137,7 @@ async function seed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 5. Event Types ---
|
// Seed event types
|
||||||
const eventTypesCSV = readCSV('eventTypes.csv');
|
const eventTypesCSV = readCSV('eventTypes.csv');
|
||||||
for (const row of eventTypesCSV) {
|
for (const row of eventTypesCSV) {
|
||||||
const presetId = resultPresetMap.get(row.resultPreset);
|
const presetId = resultPresetMap.get(row.resultPreset);
|
||||||
@@ -150,7 +155,7 @@ async function seed() {
|
|||||||
const dbEventTypes = await db.select().from(schema.eventTypes);
|
const dbEventTypes = await db.select().from(schema.eventTypes);
|
||||||
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
|
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
|
||||||
|
|
||||||
// --- 6. Registered Events ---
|
// Seed registered events
|
||||||
const eventNameMap = new Map<string, number>();
|
const eventNameMap = new Map<string, number>();
|
||||||
const registeredEventsCSV = readCSV('registeredEvents.csv');
|
const registeredEventsCSV = readCSV('registeredEvents.csv');
|
||||||
|
|
||||||
@@ -176,11 +181,11 @@ async function seed() {
|
|||||||
` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}`
|
` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Map the textual event name (e.g., "100m Sprint") to the generated DB ID
|
// Map event name|division to the generated event ID
|
||||||
eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id);
|
eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 7. Registered Players ---
|
// Seed registered players (linking players to events)
|
||||||
const dbPlayers = await db.select().from(schema.players);
|
const dbPlayers = await db.select().from(schema.players);
|
||||||
const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p]));
|
const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p]));
|
||||||
|
|
||||||
@@ -190,7 +195,6 @@ async function seed() {
|
|||||||
const divisionName = divisionNameMap.get(player?.division ?? -1);
|
const divisionName = divisionNameMap.get(player?.division ?? -1);
|
||||||
const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`);
|
const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`);
|
||||||
|
|
||||||
// Dynamic look up of the bracket row's primary key ID using the CSV text
|
|
||||||
const bracketId = bracketMap.get(row.bracket);
|
const bracketId = bracketMap.get(row.bracket);
|
||||||
|
|
||||||
if (!player) throw new Error(`Player "${row.player_registered}" not found`);
|
if (!player) throw new Error(`Player "${row.player_registered}" not found`);
|
||||||
@@ -203,7 +207,7 @@ async function seed() {
|
|||||||
await db.insert(schema.registeredPlayers).values({
|
await db.insert(schema.registeredPlayers).values({
|
||||||
playerID: player.id,
|
playerID: player.id,
|
||||||
registeredEventID: actualEventId,
|
registeredEventID: actualEventId,
|
||||||
bracket: bracketId, // Using the real relational ID instead of raw value
|
bracket: bracketId,
|
||||||
placement: row.player_placement || 0
|
placement: row.player_placement || 0
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
@@ -211,7 +215,7 @@ async function seed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-enable Foreign Key constraints now that the data is built cleanly
|
// Re-enable FK checks
|
||||||
await client.execute('PRAGMA foreign_keys = ON');
|
await client.execute('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
console.log('\n✅ Seeding complete!');
|
console.log('\n✅ Seeding complete!');
|
||||||
|
|||||||
3
src/app.d.ts
vendored
3
src/app.d.ts
vendored
@@ -1,7 +1,6 @@
|
|||||||
import type { User, Session } from 'better-auth/minimal';
|
import type { User, Session } from 'better-auth/minimal';
|
||||||
|
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// SvelteKit app type declarations
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface Locals { user?: User; session?: Session }
|
interface Locals { user?: User; session?: Session }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
const { session, user } = await validateSessionToken(token);
|
const { session, user } = await validateSessionToken(token);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
setSessionTokenCookie(event, token, session.expiresAt); // refresh cookie expiry
|
setSessionTokenCookie(event, token, session.expiresAt);
|
||||||
} else {
|
} else {
|
||||||
deleteSessionTokenCookie(event);
|
deleteSessionTokenCookie(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// Place shared exports accessible via $lib alias here
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export async function validateSessionToken(token: string) {
|
|||||||
|
|
||||||
if (!row) return { session: null, user: null };
|
if (!row) return { session: null, user: null };
|
||||||
|
|
||||||
// Expired — clean up and reject
|
// Session expired, delete it
|
||||||
if (Date.now() >= row.session.expiresAt.getTime()) {
|
if (Date.now() >= row.session.expiresAt.getTime()) {
|
||||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||||
return { session: null, user: null };
|
return { session: null, user: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sliding expiration: renew if past the halfway mark
|
// Renew session if past the halfway point
|
||||||
if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) {
|
if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) {
|
||||||
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
|
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
|
||||||
await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId));
|
await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId));
|
||||||
@@ -59,7 +59,7 @@ export function setSessionTokenCookie(event: RequestEvent, token: string, expire
|
|||||||
expires: expiresAt,
|
expires: expiresAt,
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: !import.meta.env.DEV, // allow http in local dev
|
secure: !import.meta.env.DEV,
|
||||||
sameSite: 'lax'
|
sameSite: 'lax'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { eq } from 'drizzle-orm';
|
import { sql, eq, and } from 'drizzle-orm';
|
||||||
import * as schema from '$lib/server/db/schema';
|
import * as schema from '$lib/server/db/schema';
|
||||||
import { globalEmitter } from './globalEmitter';
|
import { globalEmitter } from './globalEmitter';
|
||||||
|
|
||||||
// For page.server.ts so that it doesnt look weird before loading
|
// Initial data for page load
|
||||||
export async function getAllInitialInfo() {
|
export async function getAllInitialInfo() {
|
||||||
return {
|
return {
|
||||||
teams: await getTeams(),
|
teams: await getTeams(),
|
||||||
@@ -11,7 +11,7 @@ export async function getAllInitialInfo() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get teams object from database
|
// Fetch teams with optional filter
|
||||||
export async function getTeams(teamId?: number) {
|
export async function getTeams(teamId?: number) {
|
||||||
const allTeams = await db
|
const allTeams = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -32,7 +32,7 @@ export async function getTeams(teamId?: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all registered events from database
|
// Fetch registered events with optional filter
|
||||||
export async function getRegisteredEvents(eventId?: number) {
|
export async function getRegisteredEvents(eventId?: number) {
|
||||||
async function getWinnerInfo(teamId: number) {
|
async function getWinnerInfo(teamId: number) {
|
||||||
const teamInfo = await getTeams(teamId);
|
const teamInfo = await getTeams(teamId);
|
||||||
@@ -74,7 +74,6 @@ export async function startEvent(eventId: number) {
|
|||||||
.from(schema.registeredEventsView)
|
.from(schema.registeredEventsView)
|
||||||
.where(eq(schema.registeredEventsView.eventId, eventId));
|
.where(eq(schema.registeredEventsView.eventId, eventId));
|
||||||
let requestedEvent = event[0];
|
let requestedEvent = event[0];
|
||||||
console.log(requestedEvent);
|
|
||||||
if (requestedEvent.state != 0) {
|
if (requestedEvent.state != 0) {
|
||||||
console.log('not startable');
|
console.log('not startable');
|
||||||
return false;
|
return false;
|
||||||
@@ -84,37 +83,44 @@ export async function startEvent(eventId: number) {
|
|||||||
.set({ state: 1 })
|
.set({ state: 1 })
|
||||||
.where(eq(schema.registeredEvents.id, requestedEvent.eventId))
|
.where(eq(schema.registeredEvents.id, requestedEvent.eventId))
|
||||||
.returning();
|
.returning();
|
||||||
console.log(replacedEvent);
|
|
||||||
globalEmitter.emit('eventUpdate');
|
globalEmitter.emit('eventUpdate');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all players with an event id specified
|
// Fetch all players registered for a specific event
|
||||||
export async function getAllRegisteredEventPlayers(eventId: number) {
|
export async function getAllRegisteredEventPlayers(eventId: number, getScores?: boolean) {
|
||||||
const eventPlayers = await db
|
const eventPlayers = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.registeredEventPlayersView)
|
.from(schema.registeredEventPlayersView)
|
||||||
// where the registered player is registered for that event
|
|
||||||
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
|
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
schema.registeredEventPlayersView.bracket,
|
schema.registeredEventPlayersView.bracket,
|
||||||
schema.registeredEventPlayersView.placement,
|
sql`CASE WHEN ${schema.registeredEventPlayersView.placement} = 0 THEN 999999 ELSE ${schema.registeredEventPlayersView.placement} END ASC`,
|
||||||
schema.registeredEventPlayersView.teamName
|
schema.registeredEventPlayersView.teamName
|
||||||
);
|
);
|
||||||
return {
|
|
||||||
eventPlayers: eventPlayers.map((players) => ({
|
// 1. Wrap the map in Promise.all and await it
|
||||||
|
const resolvedPlayers = await Promise.all(
|
||||||
|
eventPlayers.map(async (players) => ({
|
||||||
id: players.playerId,
|
id: players.playerId,
|
||||||
firstName: players.firstName,
|
firstName: players.firstName,
|
||||||
lastName: players.lastName,
|
lastName: players.lastName,
|
||||||
|
registeredPlayerId: players.registeredPlayerId,
|
||||||
placement: players.placement,
|
placement: players.placement,
|
||||||
bracket: players.bracket,
|
bracket: players.bracket,
|
||||||
eventId: players.eventId,
|
eventId: players.eventId,
|
||||||
eventName: players.eventName,
|
eventName: players.eventName,
|
||||||
teamId: players.teamId,
|
teamId: players.teamId,
|
||||||
teamName: players.teamName,
|
teamName: players.teamName,
|
||||||
teamColor: players.teamColor
|
teamColor: players.teamColor,
|
||||||
|
playerScores: getScores == true ? await getPlayerScores(players.playerId, eventId) : undefined
|
||||||
}))
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Return the fully resolved data
|
||||||
|
return {
|
||||||
|
eventPlayers: resolvedPlayers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +132,43 @@ export async function getAllBrackets() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPlayerInfo(playerId: number) {
|
||||||
|
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id, playerId));
|
||||||
|
const teamInfo = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.teams)
|
||||||
|
.where(eq(schema.teams.id, playerInfo[0].team));
|
||||||
|
return { ...playerInfo[0], teamInfo: teamInfo[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerScores(playerId: number, eventId?: number) {
|
||||||
|
const playerRegistrations = await getPlayerRegistrations(playerId, eventId);
|
||||||
|
let scoresObject: any[] = [];
|
||||||
|
for (let registration in playerRegistrations) {
|
||||||
|
let currentReg = playerRegistrations[registration];
|
||||||
|
let scores = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredResults)
|
||||||
|
.where(eq(schema.registeredResults.registeredPlayerId, currentReg.registeredPlayerId));
|
||||||
|
scoresObject.push(...scores);
|
||||||
|
}
|
||||||
|
return scoresObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerRegistrations(playerId: number, eventId?: number) {
|
||||||
|
const playerRegistrations = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.registeredEventPlayersView)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.registeredEventPlayersView.playerId, playerId),
|
||||||
|
eventId ? eq(schema.registeredEventPlayersView.eventId, eventId) : undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return playerRegistrations;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getResultPreset(presetId?: number) {
|
export async function getResultPreset(presetId?: number) {
|
||||||
const resultPresets = await db
|
const resultPresets = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -137,48 +180,37 @@ export async function getResultPreset(presetId?: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moved the function the registeredEvents endpoint
|
// Merge events, players, brackets, and presets into a frontend-ready structure
|
||||||
// Just merges the results of the previous 3 functions into a standard format
|
|
||||||
export async function getRegisteredEventsWithPlayers(eventId?: number) {
|
export async function getRegisteredEventsWithPlayers(eventId?: number) {
|
||||||
// Get updated events from database
|
|
||||||
let newEvents = await getRegisteredEvents(eventId);
|
let newEvents = await getRegisteredEvents(eventId);
|
||||||
let registeredEventList = newEvents['events'];
|
let registeredEventList = newEvents['events'];
|
||||||
|
|
||||||
// Get all possible brackets from the database
|
|
||||||
let brackets = await getAllBrackets();
|
let brackets = await getAllBrackets();
|
||||||
|
|
||||||
// Initilise the final eventList
|
|
||||||
let fullEventList: any[] = [];
|
let fullEventList: any[] = [];
|
||||||
|
|
||||||
// For every event
|
|
||||||
for (let registeredEvent in registeredEventList) {
|
for (let registeredEvent in registeredEventList) {
|
||||||
let event = registeredEventList[registeredEvent];
|
let event = registeredEventList[registeredEvent];
|
||||||
|
|
||||||
// Get the info about the result preset for the ui
|
|
||||||
let resultPreset = await getResultPreset(event.resultPreset);
|
let resultPreset = await getResultPreset(event.resultPreset);
|
||||||
|
let registeredPlayers = await getAllRegisteredEventPlayers(
|
||||||
|
event.id,
|
||||||
|
eventId != undefined ? true : undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Get all players for the event
|
// Group players by bracket category for the frontend
|
||||||
let registeredPlayers = await getAllRegisteredEventPlayers(event.id);
|
|
||||||
|
|
||||||
// Map the players into an [] with structure {id: number, name: string, items: any[]}
|
|
||||||
// So that the players are sorted by bracket for the frontend
|
|
||||||
const bracketOrder = brackets.brackets.map((category) => {
|
const bracketOrder = brackets.brackets.map((category) => {
|
||||||
return {
|
return {
|
||||||
...category,
|
...category,
|
||||||
// Filter the items that match the current bracket name
|
|
||||||
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
|
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// append the player info and result preset to the event object
|
|
||||||
let eventWithPlayers = {
|
let eventWithPlayers = {
|
||||||
...event,
|
...event,
|
||||||
registeredPlayers: bracketOrder,
|
registeredPlayers: bracketOrder,
|
||||||
...resultPreset
|
...resultPreset
|
||||||
};
|
};
|
||||||
// combine all of the events into one array
|
|
||||||
fullEventList.push(eventWithPlayers);
|
fullEventList.push(eventWithPlayers);
|
||||||
}
|
}
|
||||||
// Send to client
|
|
||||||
return fullEventList;
|
return fullEventList;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const scorers = sqliteTable('users', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const sessions = sqliteTable('sessions', {
|
export const sessions = sqliteTable('sessions', {
|
||||||
id: text('id').primaryKey(), // SHA-256 hash of the token, never the raw token
|
id: text('id').primaryKey(),
|
||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => scorers.id),
|
.references(() => scorers.id),
|
||||||
@@ -160,6 +160,7 @@ export const registeredEventPlayersView = sqliteView('registeredEventPlayersView
|
|||||||
return qb
|
return qb
|
||||||
.select({
|
.select({
|
||||||
playerId: players.id,
|
playerId: players.id,
|
||||||
|
registeredPlayerId: registeredPlayers.id,
|
||||||
firstName: players.firstName,
|
firstName: players.firstName,
|
||||||
lastName: players.lastName,
|
lastName: players.lastName,
|
||||||
placement: registeredPlayers.placement,
|
placement: registeredPlayers.placement,
|
||||||
|
|||||||
@@ -1,34 +1,77 @@
|
|||||||
export async function generateEndpoint(
|
export async function generateEndpoint(
|
||||||
startFunction?: (enqueue: (data: any) => void) => void | Promise<void | (() => void)>
|
startFunction?: (enqueue: (data: any) => void) => void | Promise<void | (() => void)>,
|
||||||
|
request?: Request
|
||||||
) {
|
) {
|
||||||
let streamController: ReadableStreamDefaultController | null = null;
|
let streamController: ReadableStreamDefaultController | null = null;
|
||||||
let cleanupFunction: (() => void) | void = undefined;
|
let cleanupFunction: (() => void) | void = undefined;
|
||||||
|
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let cleanedUp = false;
|
||||||
|
|
||||||
|
const safeCleanup = () => {
|
||||||
|
if (cleanedUp) return;
|
||||||
|
cleanedUp = true;
|
||||||
|
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
|
heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cleanupFunction === 'function') {
|
||||||
|
cleanupFunction();
|
||||||
|
cleanupFunction = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const enqueue = (data: any) => {
|
const enqueue = (data: any) => {
|
||||||
|
if (cleanedUp) return;
|
||||||
let transferdata = JSON.stringify(data);
|
let transferdata = JSON.stringify(data);
|
||||||
// stringify data and add to controller queue
|
|
||||||
if (streamController) {
|
if (streamController) {
|
||||||
streamController.enqueue(`data: ${transferdata}\n\n`);
|
try {
|
||||||
} else {
|
streamController.enqueue(`data: ${transferdata}\n\n`);
|
||||||
console.log('no controller');
|
} catch {
|
||||||
|
safeCleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
streamController = controller;
|
streamController = controller;
|
||||||
|
|
||||||
|
heartbeatInterval = setInterval(() => {
|
||||||
|
try {
|
||||||
|
streamController!.enqueue(': keepalive\n\n');
|
||||||
|
} catch {
|
||||||
|
safeCleanup();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
heartbeatInterval.unref();
|
||||||
|
|
||||||
if (startFunction) {
|
if (startFunction) {
|
||||||
const result = await startFunction(enqueue);
|
try {
|
||||||
if (typeof result === 'function') {
|
const result = await startFunction(enqueue);
|
||||||
cleanupFunction = result;
|
if (typeof result === 'function') {
|
||||||
|
cleanupFunction = result;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
safeCleanup();
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async cancel() {
|
async cancel() {
|
||||||
if (cleanupFunction) cleanupFunction();
|
safeCleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (request?.signal) {
|
||||||
|
if (request.signal.aborted) {
|
||||||
|
safeCleanup();
|
||||||
|
} else {
|
||||||
|
request.signal.addEventListener('abort', () => safeCleanup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: new Response(stream, {
|
response: new Response(stream, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
// Main emitter for everything
|
|
||||||
|
|
||||||
export const globalEmitter = new EventEmitter();
|
export const globalEmitter = new EventEmitter();
|
||||||
|
globalEmitter.setMaxListeners(1000);
|
||||||
|
|||||||
@@ -13,33 +13,26 @@
|
|||||||
let containerRef = $state<HTMLDivElement | null>(null);
|
let containerRef = $state<HTMLDivElement | null>(null);
|
||||||
let activeId = $state<string | number | null>(null);
|
let activeId = $state<string | number | null>(null);
|
||||||
|
|
||||||
/**
|
/** Scroll a specific row to the center of the viewport */
|
||||||
* Public function to scroll a specific row into the center of the viewport.
|
|
||||||
* Can be called manually by the parent component.
|
|
||||||
*/
|
|
||||||
export function scrollToId(id: string | number) {
|
export function scrollToId(id: string | number) {
|
||||||
if (!containerRef) return;
|
if (!containerRef) return;
|
||||||
|
|
||||||
const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null;
|
const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null;
|
||||||
if (targetRow) {
|
if (targetRow) {
|
||||||
// Update local state to highlight the centered row
|
|
||||||
activeId = id;
|
activeId = id;
|
||||||
|
|
||||||
// Calculate the midpoint math to center the row
|
|
||||||
const rowOffsetTop = targetRow.offsetTop;
|
const rowOffsetTop = targetRow.offsetTop;
|
||||||
const rowHeight = targetRow.offsetHeight;
|
const rowHeight = targetRow.offsetHeight;
|
||||||
const containerHeight = containerRef.clientHeight;
|
|
||||||
|
|
||||||
// Target = Row position - (half of container space) + (half of row height adjustment)
|
|
||||||
const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2;
|
const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2;
|
||||||
|
|
||||||
// Using behavior: 'auto' for an instantaneous snap instead of smooth scrolling
|
|
||||||
containerRef.scrollTo({
|
containerRef.scrollTo({
|
||||||
top: centerScrollTarget,
|
top: centerScrollTarget,
|
||||||
behavior: 'auto'
|
behavior: 'auto'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={containerRef} class="table-container" style="max-height: {maxHeight};">
|
<div bind:this={containerRef} class="table-container" style="max-height: {maxHeight};">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const fitText: Action<HTMLElement> = (node) => {
|
|||||||
node.style.transformOrigin = 'top left';
|
node.style.transformOrigin = 'top left';
|
||||||
node.style.transform = 'none';
|
node.style.transform = 'none';
|
||||||
|
|
||||||
// Step 1: fit to height
|
// Fit font size to container height
|
||||||
let size = 1;
|
let size = 1;
|
||||||
node.style.fontSize = size + 'px';
|
node.style.fontSize = size + 'px';
|
||||||
while (node.scrollHeight <= container!.clientHeight) {
|
while (node.scrollHeight <= container!.clientHeight) {
|
||||||
@@ -19,7 +19,7 @@ export const fitText: Action<HTMLElement> = (node) => {
|
|||||||
}
|
}
|
||||||
node.style.fontSize = size - 1 + 'px';
|
node.style.fontSize = size - 1 + 'px';
|
||||||
|
|
||||||
// Step 2: stretch width to fill container
|
// Stretch width to fill container
|
||||||
const scaleX = container!.clientWidth / node.scrollWidth;
|
const scaleX = container!.clientWidth / node.scrollWidth;
|
||||||
node.style.transform = `scaleX(${scaleX})`;
|
node.style.transform = `scaleX(${scaleX})`;
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/routes/+layout.server.ts
Normal file
15
src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// alternative, if role isn't on locals.user
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) return { user: null };
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
|
||||||
|
};
|
||||||
@@ -1,22 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
<div class="header goldman flex h-15 w-full">
|
<div class="header goldman flex h-15 w-full">
|
||||||
<a
|
<a
|
||||||
class="align-text-middle justify-left mx-3 my-1 h-auto content-center rounded-sm border-2 border-solid border-red-500 px-2"
|
class="align-text-middle justify-left mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
href="/">home</a
|
href="/">home</a
|
||||||
>
|
>
|
||||||
<div class="w-full"></div>
|
<div class="w-full"></div>
|
||||||
<a
|
|
||||||
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2 border-solid border-red-500 px-2"
|
{#if data.user?.role === 'admin'}
|
||||||
href="/login">login</a
|
<a
|
||||||
>
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/ledger">ledger</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if data.user}
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/login">logout</a
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
|
||||||
|
border-solid border-red-500 px-2"
|
||||||
|
href="/login">login</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getAllInitialInfo } from '$lib/server/databaseManager';
|
import { getAllInitialInfo } from '$lib/server/databaseManager';
|
||||||
|
|
||||||
// Literally only here so that the frontend has the right structure
|
// Provide initial data for the home page
|
||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
return await getAllInitialInfo();
|
return await getAllInitialInfo();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,21 +14,18 @@
|
|||||||
let scoreEndpoint: EventSource;
|
let scoreEndpoint: EventSource;
|
||||||
let eventEndpoint: EventSource;
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
// Ref map for event card DOM nodes
|
// Map event IDs to their DOM elements for scrolling
|
||||||
let eventRefs = $state<Record<number, HTMLElement>>({});
|
let eventRefs = $state<Record<number, HTMLElement>>({});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Get the teams endpoint
|
// Subscribe to live team score updates via SSE
|
||||||
scoreEndpoint = new EventSource('/api/teams');
|
scoreEndpoint = new EventSource('/api/teams');
|
||||||
// When the endpoint sends something
|
|
||||||
scoreEndpoint.onmessage = (e) => {
|
scoreEndpoint.onmessage = (e) => {
|
||||||
// Parse the json
|
|
||||||
const teamsData = JSON.parse(e.data);
|
const teamsData = JSON.parse(e.data);
|
||||||
// If its teams info, then update the teams thing
|
|
||||||
if (teamsData['teams']) teams = teamsData['teams'];
|
if (teamsData['teams']) teams = teamsData['teams'];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basically same for events
|
// Subscribe to live event updates via SSE
|
||||||
eventEndpoint = new EventSource('/api/registeredEvents');
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
eventEndpoint.onmessage = (e) => {
|
eventEndpoint.onmessage = (e) => {
|
||||||
eventTable = JSON.parse(e.data);
|
eventTable = JSON.parse(e.data);
|
||||||
@@ -50,22 +47,29 @@
|
|||||||
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When focusEventId changes, scroll to and highlight that event card
|
function sortPlayers(items: any) {
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
// If a is unranked and b is ranked, move a down
|
||||||
|
if (a.placement === 0 && b.placement !== 0) return 1;
|
||||||
|
// If a is ranked and b is unranked, move a up
|
||||||
|
if (a.placement !== 0 && b.placement === 0) return -1;
|
||||||
|
// If both are ranked, sort numerically ascending (1st, 2nd, 3rd...)
|
||||||
|
return a.placement - b.placement;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to and highlight the currently ongoing event
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (focusEventId == null) return;
|
if (focusEventId == null) return;
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
// Get focused element
|
|
||||||
const el = eventRefs[focusEventId!];
|
const el = eventRefs[focusEventId!];
|
||||||
// Scroll it to the top of the box
|
|
||||||
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
|
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
|
||||||
// Wait for that to finish
|
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
// Scroll the window back to the top
|
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Svelte action: measures text overflow and drives CSS marquee
|
// Svelte action that enables CSS marquee when text overflows
|
||||||
function marquee(node: HTMLElement) {
|
function marquee(node: HTMLElement) {
|
||||||
function measure() {
|
function measure() {
|
||||||
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
@@ -88,9 +92,9 @@
|
|||||||
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
|
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- ═══════════ LEADERBOARD ═══════════ -->
|
<!-- LEADERBOARD -->
|
||||||
<section class="leaderboard">
|
<section class="leaderboard">
|
||||||
<!-- Winner — always full-width -->
|
<!-- Winner, always full-width -->
|
||||||
{#if leaderboard[0]}
|
{#if leaderboard[0]}
|
||||||
{@const team = leaderboard[0]}
|
{@const team = leaderboard[0]}
|
||||||
<a
|
<a
|
||||||
@@ -122,7 +126,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Runners-up: stretch to fill row, max 5 wide, 1 col on small -->
|
<!-- Runners-up, responsive grid -->
|
||||||
{#if leaderboard.length > 1}
|
{#if leaderboard.length > 1}
|
||||||
<div class="runners-grid" style="--runner-count:{Math.min(leaderboard.length - 1, 5)}">
|
<div class="runners-grid" style="--runner-count:{Math.min(leaderboard.length - 1, 5)}">
|
||||||
{#each leaderboard.slice(1) as team, i (team.name)}
|
{#each leaderboard.slice(1) as team, i (team.name)}
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════ EVENTS TABLE ═══════════ -->
|
<!-- EVENTS TABLE -->
|
||||||
<p class="section-label">Events</p>
|
<p class="section-label">Events</p>
|
||||||
|
|
||||||
<section class="events-scroll">
|
<section class="events-scroll">
|
||||||
@@ -167,7 +171,9 @@
|
|||||||
<div
|
<div
|
||||||
class="event-card"
|
class="event-card"
|
||||||
class:ongoing-event={event.state == 1}
|
class:ongoing-event={event.state == 1}
|
||||||
|
class:completed-event={event.state == 2}
|
||||||
bind:this={eventRefs[event.id]}
|
bind:this={eventRefs[event.id]}
|
||||||
|
style={event.state == 2 ? `--event-color: ${event.winner.color}` : ''}
|
||||||
>
|
>
|
||||||
<div class="event-header">
|
<div class="event-header">
|
||||||
<a href="/event/{event.id}" class="event-name goldman">{event.name}</a>
|
<a href="/event/{event.id}" class="event-name goldman">{event.name}</a>
|
||||||
@@ -177,7 +183,10 @@
|
|||||||
{:else if event.state == 2}
|
{:else if event.state == 2}
|
||||||
<span
|
<span
|
||||||
class="event-winner event-status goldman"
|
class="event-winner event-status goldman"
|
||||||
style="--winner-color:{event.winner.color}">Won By {event.winner.name}</span
|
style="--winner-color:{event.winner.color}"
|
||||||
|
>Won By {event.winner.name} at {new Date(event.completed)
|
||||||
|
.toLocaleTimeString()
|
||||||
|
.slice(0, -3)}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +201,7 @@
|
|||||||
<span class="brackets-name-text align-text-middle">{bracket.name}</span>
|
<span class="brackets-name-text align-text-middle">{bracket.name}</span>
|
||||||
<div class="bracket-vertical-sep"></div>
|
<div class="bracket-vertical-sep"></div>
|
||||||
</div>
|
</div>
|
||||||
{#each bracket.items as player}
|
{#each sortPlayers(bracket.items) as player}
|
||||||
<div class="player-box" style="--c:{player.teamColor}">
|
<div class="player-box" style="--c:{player.teamColor}">
|
||||||
<div class="player-ghost" aria-hidden="true">
|
<div class="player-ghost" aria-hidden="true">
|
||||||
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
@@ -208,10 +217,10 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-name-wrap" use:marquee>
|
<div class="player-name-wrap" use:marquee>
|
||||||
<span class="marquee-inner">
|
<a href="/stats/player/{player.id}" class="marquee-inner">
|
||||||
{player.firstName}
|
{player.firstName}
|
||||||
{player.lastName}
|
{player.lastName}
|
||||||
</span>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#if player.placement !== 0}
|
{#if player.placement !== 0}
|
||||||
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
||||||
|
|||||||
@@ -5,32 +5,25 @@ import * as schema from '$lib/server/db/schema';
|
|||||||
import { getRegisteredEvents } from '$lib/server/databaseManager';
|
import { getRegisteredEvents } from '$lib/server/databaseManager';
|
||||||
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// Decode body
|
|
||||||
let responseBody = await request.json();
|
let responseBody = await request.json();
|
||||||
|
|
||||||
// If there is no request then dont respond
|
|
||||||
if (!responseBody) {
|
if (!responseBody) {
|
||||||
return new Error('nuh uh');
|
return new Error('nuh uh');
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(responseBody));
|
|
||||||
if (responseBody.eventId) {
|
if (responseBody.eventId) {
|
||||||
// Get the event
|
|
||||||
let eventData = await getRegisteredEvents(responseBody.eventId);
|
let eventData = await getRegisteredEvents(responseBody.eventId);
|
||||||
|
|
||||||
// If the event hasnt started or ended
|
|
||||||
if (eventData.events[0].state != 1) {
|
if (eventData.events[0].state != 1) {
|
||||||
return new Error();
|
return new Error();
|
||||||
}
|
}
|
||||||
let scoringPreset = eventData.events[0].scoringPreset;
|
let scoringPreset = eventData.events[0].scoringPreset;
|
||||||
console.log(scoringPreset);
|
|
||||||
|
|
||||||
// make a new main ledger entry
|
// Create ledger entry to record this scoring event
|
||||||
let newLedgerEntry = await db
|
let newLedgerEntry = await db
|
||||||
.insert(schema.mainLedger)
|
.insert(schema.mainLedger)
|
||||||
.values({ registeredEvent: responseBody.eventId })
|
.values({ registeredEvent: responseBody.eventId })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// get the id so i can use it in the sub ledgers
|
|
||||||
let ledgerEntryId = newLedgerEntry[0].id;
|
let ledgerEntryId = newLedgerEntry[0].id;
|
||||||
|
|
||||||
function getPoints(
|
function getPoints(
|
||||||
@@ -45,41 +38,80 @@ export async function POST({ request }: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for every bracket and player
|
// Accumulate scores per team, then batch insert
|
||||||
|
const teamScores = new Map<any, number>();
|
||||||
|
|
||||||
for (let bracket in responseBody.brackets) {
|
for (let bracket in responseBody.brackets) {
|
||||||
for (let player in responseBody.brackets[bracket].players) {
|
for (let player in responseBody.brackets[bracket].players) {
|
||||||
// variable fun
|
|
||||||
let currentPlayer = responseBody.brackets[bracket].players[player];
|
let currentPlayer = responseBody.brackets[bracket].players[player];
|
||||||
console.log(currentPlayer);
|
|
||||||
let currentPlayerTeam = currentPlayer.teamId;
|
let currentPlayerTeam = currentPlayer.teamId;
|
||||||
let currentPlayerPosition = currentPlayer.position;
|
let currentPlayerPosition = currentPlayer.position;
|
||||||
|
|
||||||
// If they put in a score
|
|
||||||
if (currentPlayerPosition > 0) {
|
if (currentPlayerPosition > 0) {
|
||||||
let score = getPoints(scoringPreset, currentPlayerPosition);
|
let score = getPoints(scoringPreset, currentPlayerPosition);
|
||||||
// If their score is in the preset and they put in a score
|
|
||||||
if (currentPlayer.scores.length > 0) {
|
if (currentPlayer.scores.length > 0) {
|
||||||
if (score > 0) {
|
if (score > 0) {
|
||||||
// put the scores on the board baby
|
const currentTeamScore = teamScores.get(currentPlayerTeam) || 0;
|
||||||
// THIS SHOULDNT BE REFERENCED THIS IS INTENDED
|
teamScores.set(currentPlayerTeam, currentTeamScore + score);
|
||||||
let newScoreLedgerEntry = await db
|
|
||||||
.insert(schema.scoreLedger)
|
|
||||||
.values({ ledgerID: ledgerEntryId, teamID: currentPlayerTeam, points: score });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let result in currentPlayer.scores) {
|
||||||
|
let currentResult = currentPlayer.scores[result];
|
||||||
|
let newScoreEntry = await db
|
||||||
|
.insert(schema.registeredResults)
|
||||||
|
.values({
|
||||||
|
registeredPlayerId: currentPlayer.registeredPlayerId,
|
||||||
|
resultIndex: parseInt(result),
|
||||||
|
result: currentResult
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
console.log(newScoreEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player placement in the database
|
||||||
let newPlayerPlacement = await db
|
let newPlayerPlacement = await db
|
||||||
.update(schema.registeredPlayers)
|
.update(schema.registeredPlayers)
|
||||||
.set({ placement: currentPlayerPosition })
|
.set({ placement: currentPlayerPosition })
|
||||||
.where(eq(schema.registeredPlayers.playerID, currentPlayer.id))
|
.where(eq(schema.registeredPlayers.id, currentPlayer.registeredPlayerId))
|
||||||
.returning();
|
.returning();
|
||||||
console.log(newPlayerPlacement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch insert team scores into the ledger
|
||||||
|
if (teamScores.size > 0) {
|
||||||
|
const ledgerEntries = Array.from(teamScores.entries()).map(([teamID, points]) => ({
|
||||||
|
ledgerID: ledgerEntryId,
|
||||||
|
teamID,
|
||||||
|
points
|
||||||
|
}));
|
||||||
|
|
||||||
|
let newScores = await db.insert(schema.scoreLedger).values(ledgerEntries).returning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the winning team from accumulated scores
|
||||||
|
let highestScore = -1;
|
||||||
|
let winningTeamId = null;
|
||||||
|
|
||||||
|
for (let [teamID, points] of teamScores.entries()) {
|
||||||
|
if (points > highestScore) {
|
||||||
|
highestScore = points;
|
||||||
|
winningTeamId = teamID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (winningTeamId) {
|
||||||
|
let teamWinnerUpdate = await db
|
||||||
|
.update(schema.registeredEvents)
|
||||||
|
.set({ teamWinner: winningTeamId, state: 2, timeCompleted: Date.now() })
|
||||||
|
.where(eq(schema.registeredEvents.id, responseBody.eventId))
|
||||||
|
.returning();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Update the frontends
|
|
||||||
globalEmitter.emit('scoreUpdate');
|
globalEmitter.emit('scoreUpdate');
|
||||||
// Return a resonse because
|
globalEmitter.emit('eventUpdate');
|
||||||
return new Response('coolsies');
|
return new Response('coolsies');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/routes/api/logout/+server.ts
Normal file
12
src/routes/api/logout/+server.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async (event) => {
|
||||||
|
if (event.locals.session) {
|
||||||
|
await invalidateSession(event.locals.session.id);
|
||||||
|
}
|
||||||
|
deleteSessionTokenCookie(event);
|
||||||
|
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
};
|
||||||
@@ -2,44 +2,32 @@ import { getRegisteredEventsWithPlayers } from '$lib/server/databaseManager';
|
|||||||
import { globalEmitter } from '$lib/server/globalEmitter';
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET({ request }) {
|
||||||
// Generate stream endpoint
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
const endpoint = generateEndpoint(async (enqueue) => {
|
|
||||||
// Get the all the events with the players seperated into brackets
|
|
||||||
let eventList = async () => {
|
let eventList = async () => {
|
||||||
// Get eventList with structure from database
|
|
||||||
let newEventList = await getRegisteredEventsWithPlayers();
|
let newEventList = await getRegisteredEventsWithPlayers();
|
||||||
// send to client
|
|
||||||
enqueue(newEventList);
|
enqueue(newEventList);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the eventList to the client when a connection is made
|
// Send initial data on connection, then subscribe to updates
|
||||||
// TODO make it so that this only happens on an initial post request
|
|
||||||
eventList();
|
eventList();
|
||||||
// When the data changes send an update to the client
|
|
||||||
globalEmitter.on('eventUpdate', eventList);
|
globalEmitter.on('eventUpdate', eventList);
|
||||||
|
|
||||||
// Return cleanup function to remove listener when it closes
|
|
||||||
return () => {
|
return () => {
|
||||||
globalEmitter.off('eventUpdate', eventList);
|
globalEmitter.off('eventUpdate', eventList);
|
||||||
};
|
};
|
||||||
});
|
}, request);
|
||||||
return (await endpoint).response;
|
return (await endpoint).response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// Decode body
|
|
||||||
let responseBody = await request.json();
|
let responseBody = await request.json();
|
||||||
|
|
||||||
// If there is no request then dont respond
|
|
||||||
if (!responseBody) {
|
if (!responseBody) {
|
||||||
return new Response('nuh uh');
|
return new Response('nuh uh');
|
||||||
} else {
|
} else {
|
||||||
// Get requested event
|
|
||||||
let eventRequested = responseBody.eventId;
|
let eventRequested = responseBody.eventId;
|
||||||
// request eventList from database
|
|
||||||
let eventList = await getRegisteredEventsWithPlayers(eventRequested);
|
let eventList = await getRegisteredEventsWithPlayers(eventRequested);
|
||||||
// return eventList to client
|
|
||||||
return new Response(JSON.stringify(eventList));
|
return new Response(JSON.stringify(eventList));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager';
|
|||||||
import { globalEmitter } from '$lib/server/globalEmitter';
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
// Expose post request
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// When post request recieved increment testscores by 1
|
|
||||||
// Return ok so the frontend is happy
|
|
||||||
return new Response('ok');
|
return new Response('ok');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,25 @@ import { getTeams } from '$lib/server/databaseManager';
|
|||||||
import { globalEmitter } from '$lib/server/globalEmitter';
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
// Expose post request
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// When post request recieved increment testscores by 1
|
|
||||||
globalEmitter.emit('incrementScores');
|
globalEmitter.emit('incrementScores');
|
||||||
// Return ok so the frontend is happy
|
|
||||||
return new Response('ok');
|
return new Response('ok');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET({ request }) {
|
||||||
const endpoint = generateEndpoint(async (enqueue) => {
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
// Function to grab score from database and add it to message queue
|
|
||||||
let newScore = async () => {
|
let newScore = async () => {
|
||||||
let newScores = await getTeams();
|
let newScores = await getTeams();
|
||||||
enqueue(newScores);
|
enqueue(newScores);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial Sync
|
// Send initial team scores, then subscribe to updates
|
||||||
newScore();
|
newScore();
|
||||||
globalEmitter.on('scoreUpdate', newScore);
|
globalEmitter.on('scoreUpdate', newScore);
|
||||||
|
|
||||||
// Simply return the cleanup function here
|
|
||||||
return () => {
|
return () => {
|
||||||
globalEmitter.off('scoreUpdate', newScore);
|
globalEmitter.off('scoreUpdate', newScore);
|
||||||
};
|
};
|
||||||
});
|
}, request);
|
||||||
return (await endpoint).response;
|
return (await endpoint).response;
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/routes/event/[eventId]/+page.server.ts
Normal file
14
src/routes/event/[eventId]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) return { user: null };
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
let { params }: PageProps = $props();
|
let { params, data }: PageProps = $props();
|
||||||
|
|
||||||
function ordinal(n: number) {
|
function ordinal(n: number) {
|
||||||
const s = ['th', 'st', 'nd', 'rd'];
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
@@ -41,7 +41,22 @@
|
|||||||
'Content-type': 'application/json; charset=UTF-8'
|
'Content-type': 'application/json; charset=UTF-8'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return response.json();
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Sort players: placed first (ascending), unplaced last
|
||||||
|
if (data && data[0] && data[0].registeredPlayers) {
|
||||||
|
data[0].registeredPlayers.forEach((bracket: any) => {
|
||||||
|
bracket.items.sort((a: any, b: any) => {
|
||||||
|
if (a.placement === 0 && b.placement === 0) return 0;
|
||||||
|
if (a.placement === 0) return 1;
|
||||||
|
if (b.placement === 0) return -1;
|
||||||
|
return a.placement - b.placement;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventDataPromise = getEventData();
|
let eventDataPromise = getEventData();
|
||||||
@@ -60,12 +75,21 @@
|
|||||||
<div>loading</div>
|
<div>loading</div>
|
||||||
{:then eventData}
|
{:then eventData}
|
||||||
{@const event = eventData[0]}
|
{@const event = eventData[0]}
|
||||||
|
{console.log(event)}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="w-full flex-col px-[5vw] text-center">
|
<div class="w-full flex-col px-[5vw] text-center">
|
||||||
<div
|
<div
|
||||||
|
style:background-color={event.state === 1
|
||||||
|
? 'color-mix(in srgb, #fe640b 18%, transparent)'
|
||||||
|
: event.state === 2
|
||||||
|
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
|
||||||
|
: ''}
|
||||||
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
||||||
>
|
>
|
||||||
{event.name} - {event.division}
|
{event.name} - {event.division}
|
||||||
|
{#if event.state == 1}- ONGOING
|
||||||
|
{:else if event.state == 2}- FINISHED
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#each event.registeredPlayers as bracket, bi}
|
{#each event.registeredPlayers as bracket, bi}
|
||||||
{#if bi > 0}
|
{#if bi > 0}
|
||||||
@@ -77,7 +101,7 @@
|
|||||||
<div class="bracket-vertical-sep"></div>
|
<div class="bracket-vertical-sep"></div>
|
||||||
</div>
|
</div>
|
||||||
{#each bracket.items as player}
|
{#each bracket.items as player}
|
||||||
<div class="player-box h-30" style="--c:{player.teamColor}">
|
<div class="player-box" style="--c:{player.teamColor}">
|
||||||
<div class="player-ghost" aria-hidden="true">
|
<div class="player-ghost" aria-hidden="true">
|
||||||
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
<text
|
<text
|
||||||
@@ -99,6 +123,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if player.placement !== 0}
|
{#if player.placement !== 0}
|
||||||
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
<div class="player-placement goldman">{ordinal(player.placement)}</div>
|
||||||
|
<div class="resultContainer flex justify-center">
|
||||||
|
{#each player.playerScores as score}
|
||||||
|
<div class="mx-5 my-1 rounded border-2" style="border-color:{player.teamColor}">
|
||||||
|
Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="player-placement-gap"></div>
|
<div class="player-placement-gap"></div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -108,10 +139,23 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-10 flex w-full justify-center">
|
{#if data.user}
|
||||||
<a
|
<div class="mt-10 flex w-full justify-center">
|
||||||
class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4"
|
<a
|
||||||
href="/event/scoring/{eventId}">Score This Event</a
|
class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4"
|
||||||
>
|
href="/event/scoring/{eventId}">Score This Event</a
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.resultContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.resultContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
return { destroy: () => ro.disconnect() };
|
return { destroy: () => ro.disconnect() };
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventId = params.eventId;
|
let eventId = parseInt(params.eventId);
|
||||||
let eventEndpoint: EventSource;
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
type Player = { firstName: string; lastName: string; teamColor: string; [key: string]: any };
|
type Player = { firstName: string; lastName: string; teamColor: string; [key: string]: any };
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
|
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
|
||||||
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
|
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
|
||||||
|
|
||||||
// Drag state
|
|
||||||
let dragSrc = $state<{ bi: number; pi: number } | null>(null);
|
let dragSrc = $state<{ bi: number; pi: number } | null>(null);
|
||||||
|
|
||||||
let sortByScore = $state(true);
|
let sortByScore = $state(true);
|
||||||
@@ -93,6 +92,17 @@
|
|||||||
...bracket,
|
...bracket,
|
||||||
items: sortByScore
|
items: sortByScore
|
||||||
? [...bracket.items].sort((a, b) => {
|
? [...bracket.items].sort((a, b) => {
|
||||||
|
const scoresA = committedScores[a.id]?.filter((s) => s !== null) ?? [];
|
||||||
|
const scoresB = committedScores[b.id]?.filter((s) => s !== null) ?? [];
|
||||||
|
|
||||||
|
const hasA = scoresA.length > 0;
|
||||||
|
const hasB = scoresB.length > 0;
|
||||||
|
|
||||||
|
// Push unranked players to the bottom, then sort by score
|
||||||
|
if (!hasA && !hasB) return 0;
|
||||||
|
if (!hasA) return 1;
|
||||||
|
if (!hasB) return -1;
|
||||||
|
|
||||||
const fallback = lowerIsBetter ? Infinity : -Infinity;
|
const fallback = lowerIsBetter ? Infinity : -Infinity;
|
||||||
const sa = useAverage
|
const sa = useAverage
|
||||||
? average(committedScores[a.id] ?? [], fallback)
|
? average(committedScores[a.id] ?? [], fallback)
|
||||||
@@ -100,6 +110,7 @@
|
|||||||
const sb = useAverage
|
const sb = useAverage
|
||||||
? average(committedScores[b.id] ?? [], fallback)
|
? average(committedScores[b.id] ?? [], fallback)
|
||||||
: best(committedScores[b.id] ?? [], fallback);
|
: best(committedScores[b.id] ?? [], fallback);
|
||||||
|
|
||||||
return lowerIsBetter ? sa - sb : sb - sa;
|
return lowerIsBetter ? sa - sb : sb - sa;
|
||||||
})
|
})
|
||||||
: bracket.items
|
: bracket.items
|
||||||
@@ -115,7 +126,7 @@
|
|||||||
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(data);
|
console.log(data[0]);
|
||||||
event = data[0];
|
event = data[0];
|
||||||
brackets = data[0].registeredPlayers.map((b: any) => ({
|
brackets = data[0].registeredPlayers.map((b: any) => ({
|
||||||
...b,
|
...b,
|
||||||
@@ -144,11 +155,16 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
eventEndpoint = new EventSource('/api/registeredEvents');
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
eventEndpoint.onmessage = (e) => {
|
eventEndpoint.onmessage = async (e) => {
|
||||||
const data = JSON.parse(e.data)[eventId - 1];
|
const response = await fetch('/api/registeredEvents', {
|
||||||
console.log(data);
|
method: 'POST',
|
||||||
event = data;
|
body: JSON.stringify({ eventId }),
|
||||||
brackets = data.registeredPlayers.map((b: any) => ({
|
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('sone', data[0]);
|
||||||
|
event = data[0];
|
||||||
|
brackets = data[0].registeredPlayers.map((b: any) => ({
|
||||||
...b,
|
...b,
|
||||||
items: [...b.items]
|
items: [...b.items]
|
||||||
}));
|
}));
|
||||||
@@ -238,12 +254,15 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="w-full flex-col px-[2vw] text-center">
|
<div class="w-full flex-col px-[2vw] text-center">
|
||||||
<div
|
<div
|
||||||
style:background-color={event.state == 1
|
style:background-color={event.state === 1
|
||||||
? 'color-mix(in srgb, #fe640b 18%, transparent);'
|
? 'color-mix(in srgb, #fe640b 18%, transparent)'
|
||||||
: undefined}
|
: event.state === 2
|
||||||
|
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
|
||||||
|
: ''}
|
||||||
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
|
||||||
>
|
>
|
||||||
{event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING
|
{event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING
|
||||||
|
{:else if event.state == 2}- FINISHED
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -252,11 +271,6 @@
|
|||||||
>Start event</button
|
>Start event</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- <button onclick={() => (sortByScore = !sortByScore)}> -->
|
|
||||||
<!-- {sortByScore ? 'Sort: Score' : 'Sort: Manual'} -->
|
|
||||||
<!-- </button> -->
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-center">
|
<div class="flex flex-row justify-center">
|
||||||
<div class="flex w-50 min-w-0 flex-col">
|
<div class="flex w-50 min-w-0 flex-col">
|
||||||
<div class="brackets-name text-bold">=</div>
|
<div class="brackets-name text-bold">=</div>
|
||||||
@@ -319,36 +333,45 @@
|
|||||||
{player.lastName}
|
{player.lastName}
|
||||||
</div>
|
</div>
|
||||||
<div class="result-input-containers flex flex-col">
|
<div class="result-input-containers flex flex-col">
|
||||||
{#each Array.from({ length: numResults }, (_, i) => i) as run}
|
{#if event.state == 1}
|
||||||
<input
|
{#each Array.from({ length: numResults }, (_, i) => i) as run}
|
||||||
type="number"
|
<input
|
||||||
placeholder="run {run + 1}"
|
type="number"
|
||||||
disabled={event.state != 1}
|
placeholder="run {run + 1}"
|
||||||
value={pendingScores[player.id]?.[run] ?? ''}
|
class="text-black"
|
||||||
oninput={(e) => {
|
disabled={event.state != 1}
|
||||||
const current = [
|
value={pendingScores[player.id]?.[run] ?? ''}
|
||||||
...(pendingScores[player.id] ?? Array(numResults).fill(''))
|
oninput={(e) => {
|
||||||
];
|
const current = [
|
||||||
current[run] = e.currentTarget.value;
|
...(pendingScores[player.id] ?? Array(numResults).fill(''))
|
||||||
pendingScores[player.id] = current;
|
];
|
||||||
}}
|
current[run] = e.currentTarget.value;
|
||||||
onblur={(e) => {
|
pendingScores[player.id] = current;
|
||||||
const val = parseFloat(e.currentTarget.value);
|
}}
|
||||||
const current = [
|
onblur={(e) => {
|
||||||
...(committedScores[player.id] ?? Array(numResults).fill(null))
|
const val = parseFloat(e.currentTarget.value);
|
||||||
];
|
const current = [
|
||||||
current[run] = isNaN(val) ? null : val;
|
...(committedScores[player.id] ?? Array(numResults).fill(null))
|
||||||
committedScores[player.id] = current;
|
];
|
||||||
}}
|
current[run] = isNaN(val) ? null : val;
|
||||||
/>
|
committedScores[player.id] = current;
|
||||||
{/each}
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{#if event.resultPresets[0].averageResults == 1}
|
||||||
|
<div class="text-sm opacity-60">
|
||||||
|
avg: {avgScore.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if player.playerScores.length > 0}
|
||||||
|
{#each player.playerScores as score}
|
||||||
|
<div class="border-red border-2 px-2">
|
||||||
|
{score.result}{event.resultPresets[0].unit}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if event.resultPresets[0].averageResults == 1}
|
|
||||||
<div class="text-sm opacity-60">
|
|
||||||
avg: {avgScore.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Leaderboard ── */
|
/* Leaderboard */
|
||||||
.leaderboard {
|
.leaderboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
border: 2px solid var(--c);
|
border: 2px solid var(--c);
|
||||||
color: var(--c);
|
color: var(--c);
|
||||||
background: color-mix(in srgb, var(--c) 10%, transparent);
|
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||||
/* Grid stacking: ghost + fg share the same cell */
|
/* Ghost + foreground stacked in same grid cell */
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ghost SVG: fill the cell edge-to-edge, text flush to bottom */
|
/* Ghost SVG fills cell, text sits at bottom */
|
||||||
.score-ghost {
|
.score-ghost {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Foreground */
|
/* Foreground content layer */
|
||||||
.score-fg {
|
.score-fg {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -126,14 +126,7 @@
|
|||||||
font-size: clamp(26px, 7.5vw, 52px);
|
font-size: clamp(26px, 7.5vw, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */
|
||||||
* Runners-up grid:
|
|
||||||
* - 1 column on small screens (<480px)
|
|
||||||
* - 2 columns on medium (480–699px)
|
|
||||||
* - 4 columns on large (≥700px), max 5
|
|
||||||
* --runner-count drives the actual column count on large screens
|
|
||||||
* so fewer than 5 teams still fill the whole row.
|
|
||||||
*/
|
|
||||||
.runners-grid {
|
.runners-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -154,8 +147,7 @@
|
|||||||
}
|
}
|
||||||
@media (min-width: 700px) {
|
@media (min-width: 700px) {
|
||||||
.runners-grid {
|
.runners-grid {
|
||||||
/* clamp actual count to 4 on large, but use --runner-count
|
/* Use --runner-count so fewer teams fill the row evenly */
|
||||||
so e.g. 2 teams still fill 2 equal columns not 2 of 4 */
|
|
||||||
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
|
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
|
||||||
}
|
}
|
||||||
.winner {
|
.winner {
|
||||||
@@ -166,7 +158,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Section label ── */
|
/* Section label */
|
||||||
.section-label {
|
.section-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 2.5px;
|
letter-spacing: 2.5px;
|
||||||
@@ -176,7 +168,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Events scrollable container ── */
|
/* Events scrollable container */
|
||||||
.events-scroll {
|
.events-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35);
|
box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35);
|
||||||
@@ -185,7 +177,7 @@
|
|||||||
/* max-height: 900px; */
|
/* max-height: 900px; */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 14px 24px;
|
padding: 0 14px 24px;
|
||||||
/* Thin custom scrollbar */
|
/* Thin scrollbar styling */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent;
|
scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent;
|
||||||
}
|
}
|
||||||
@@ -206,7 +198,7 @@
|
|||||||
color: var(--winner-color);
|
color: var(--winner-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Event card ── */
|
/* Event card */
|
||||||
.event-card {
|
.event-card {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
@@ -218,7 +210,13 @@
|
|||||||
background-color: color-mix(in srgb, currentColor 18%, transparent);
|
background-color: color-mix(in srgb, currentColor 18%, transparent);
|
||||||
color: var(--ctp-latte-peach);
|
color: var(--ctp-latte-peach);
|
||||||
}
|
}
|
||||||
/* Focus highlight pulse — added/removed by $effect */
|
|
||||||
|
.completed-event {
|
||||||
|
border-color: var(--event-color);
|
||||||
|
/* If you want a subtle background tint like ongoing events usually have: */
|
||||||
|
background-color: color-mix(in srgb, var(--event-color) 10%, transparent);
|
||||||
|
}
|
||||||
|
/* Pulse animation for focused event card */
|
||||||
.event-card.highlight-pulse {
|
.event-card.highlight-pulse {
|
||||||
animation: card-pulse 1.2s ease-out forwards;
|
animation: card-pulse 1.2s ease-out forwards;
|
||||||
}
|
}
|
||||||
@@ -254,7 +252,7 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Brackets ── */
|
/* Brackets */
|
||||||
.brackets {
|
.brackets {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -271,9 +269,9 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Player boxes ── */
|
/* Player boxes */
|
||||||
.player-box {
|
.player-box {
|
||||||
flex: 1 1 0; /* equal widths, no min-content bias */
|
flex: 1 1 0;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -287,11 +285,11 @@
|
|||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 1 col on small screens, 4 across on large */
|
/* Mobile: single column layout */
|
||||||
@media (max-width: 479px) {
|
@media (max-width: 479px) {
|
||||||
.brackets {
|
.brackets {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch; /* was flex-start — lets columns fill full height */
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.bracket-sep {
|
.bracket-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
@@ -326,7 +324,7 @@
|
|||||||
@media (min-width: 700px) {
|
@media (min-width: 700px) {
|
||||||
.player-box {
|
.player-box {
|
||||||
max-width: calc(25% - 6px);
|
max-width: calc(25% - 6px);
|
||||||
} /* 4 per row */
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-ghost {
|
.player-ghost {
|
||||||
@@ -345,9 +343,14 @@
|
|||||||
.player-name-wrap {
|
.player-name-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
text-decoration: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-name-wrap:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
.marquee-inner {
|
.marquee-inner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -379,5 +382,5 @@
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.player-placement-gap {
|
.player-placement-gap {
|
||||||
height: 20px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/routes/ledger/+page.server.ts
Normal file
23
src/routes/ledger/+page.server.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// src/routes/admin/+page.server.ts
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { scorers } from '$lib/server/db/schema';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) {
|
||||||
|
throw redirect(303, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ role: scorers.role })
|
||||||
|
.from(scorers)
|
||||||
|
.where(eq(scorers.id, locals.user.id));
|
||||||
|
|
||||||
|
if (row?.role !== 'admin') {
|
||||||
|
throw error(403, 'Forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: locals.user };
|
||||||
|
};
|
||||||
1
src/routes/ledger/+page.svelte
Normal file
1
src/routes/ledger/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div>some</div>
|
||||||
@@ -4,7 +4,13 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { scorers } from '$lib/server/db/schema';
|
import { scorers } from '$lib/server/db/schema';
|
||||||
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
||||||
import type { Actions } from './$types';
|
import type { Actions } from '../login/$types';
|
||||||
|
|
||||||
|
import type { PageServerLoad } from '../login/$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
return { user: locals.user };
|
||||||
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActionData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<h1>Log in</h1>
|
{#if data.user}
|
||||||
|
<div class="align-center flex h-full w-full flex-col text-center">
|
||||||
|
<h1>Already logged in</h1>
|
||||||
|
<p>You're signed in as <strong>{data.user.username}</strong></p>
|
||||||
|
<a href="/">Go to home</a>
|
||||||
|
<form method="POST" action="/api/logout">
|
||||||
|
<button type="submit">Log out</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<h1>Log in</h1>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
<input class="text-black" name="username" type="text" autocomplete="username" required />
|
<input class="text-black" name="username" type="text" autocomplete="username" required />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<input
|
<input
|
||||||
class="text-black"
|
class="text-black"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if form?.message}
|
{#if form?.message}
|
||||||
<p class="error">{form.message}</p>
|
<p class="error">{form.message}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button type="submit">Log in</button>
|
<button type="submit">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
|
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { scorers } from '$lib/server/db/schema';
|
import { scorers } from '$lib/server/db/schema';
|
||||||
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
|
||||||
import type { Actions } from './$types';
|
import type { Actions } from '../signup/$types';
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
@@ -21,7 +21,7 @@ export const actions: Actions = {
|
|||||||
return fail(400, { message: 'Username already taken' });
|
return fail(400, { message: 'Username already taken' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordHash = await Bun.password.hash(password); // defaults to argon2id
|
const passwordHash = await Bun.password.hash(password);
|
||||||
const userId = crypto.randomUUID();
|
const userId = crypto.randomUUID();
|
||||||
|
|
||||||
await db.insert(scorers).values({ id: userId, username, passwordHash });
|
await db.insert(scorers).values({ id: userId, username, passwordHash });
|
||||||
@@ -9,12 +9,19 @@
|
|||||||
<form method="POST">
|
<form method="POST">
|
||||||
<label>
|
<label>
|
||||||
Username
|
Username
|
||||||
<input name="username" type="text" autocomplete="username" required />
|
<input name="username" class="text-black" type="text" autocomplete="username" required />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Password
|
Password
|
||||||
<input name="password" type="password" autocomplete="new-password" minlength="8" required />
|
<input
|
||||||
|
class="text-black"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
minlength="8"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<small>At least 8 characters</small>
|
<small>At least 8 characters</small>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
14
src/routes/stats/player/[playerId]/+page.server.ts
Normal file
14
src/routes/stats/player/[playerId]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { getPlayerInfo } from '$lib/server/databaseManager';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
// export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
// return {
|
||||||
|
// data: await getPlayerInfo(parseInt(params.playerId))
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
return {
|
||||||
|
playerInfo: getPlayerInfo(parseInt(params.playerId))
|
||||||
|
};
|
||||||
|
};
|
||||||
75
src/routes/stats/player/[playerId]/+page.svelte
Normal file
75
src/routes/stats/player/[playerId]/+page.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
function marquee(node: HTMLElement) {
|
||||||
|
function measure() {
|
||||||
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
|
if (!inner) return;
|
||||||
|
const overflow = inner.scrollWidth - node.clientWidth;
|
||||||
|
if (overflow > 2) {
|
||||||
|
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
|
||||||
|
inner.classList.add('scrolling');
|
||||||
|
} else {
|
||||||
|
inner.classList.remove('scrolling');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
measure();
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(node);
|
||||||
|
return { destroy: () => ro.disconnect() };
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await data.playerInfo}
|
||||||
|
<div>Loading...</div>
|
||||||
|
{:then playerInfo}
|
||||||
|
{console.log(playerInfo)}
|
||||||
|
<div class="mt-5 flex justify-center">
|
||||||
|
<div
|
||||||
|
class="player-single-box aspect-square w-full max-w-[95vw] justify-center md:aspect-2/1 lg:aspect-2/1"
|
||||||
|
style="--c:{playerInfo.teamInfo.color}"
|
||||||
|
>
|
||||||
|
<div class="player-ghost" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0.7"
|
||||||
|
font-size="1"
|
||||||
|
dominant-baseline="auto"
|
||||||
|
textLength="100"
|
||||||
|
lengthAdjust="spacingAndGlyphs"
|
||||||
|
font-family="'Black Ops One',system-ui">{playerInfo.firstName}</text
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class=" goldman player-name-wrap text-5xl" use:marquee>
|
||||||
|
{playerInfo.firstName}
|
||||||
|
{playerInfo.lastName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.player-single-box {
|
||||||
|
flex: 1 1 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid var(--c);
|
||||||
|
color: var(--c);
|
||||||
|
background: color-mix(in srgb, var(--c) 10%, transparent);
|
||||||
|
padding: 5px 7px 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.player-name-wrap {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,13 +3,10 @@ import adapter from '@sveltejs/adapter-auto';
|
|||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
// Force runes mode (except in node_modules). Remove when Svelte 6 makes it default.
|
||||||
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||||
},
|
},
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
Reference in New Issue
Block a user