Description

This recipe will check if today is the logged in person's birthday, anniversary or workiversary, and if so, it displays a celebration message on the page and showers the screen with confetti for a few seconds. Once the confetti settles, the user can click the message to experience the celebration all over again.

The confetti colors are different, depending on the occasion. Currently birthdays are multicolored, anniversaries are shades of red and yellow, and workiversaries are shades of blue. If the person has multiple occasions on the same day, then the confetti will be a mix of each occasion's colors. I also included 28 other color combos that I thought were interesting in the JavaScript, so if you don't like the default ones, there are plenty of others to pick from.

The intended purpose of this recipe was for staff members on the internal Rock site, but it should also work on the public website for non-staff members since the workiversary portions would just be ignored for anyone who doesn't have a staff hire date attribute value.

Credit

The actual confetti part of this recipe was adapted from this CodePen. It's not clear who the original author was, but that's where I got it from. Google shows samples of the exact same code all over the internet, so we'll probably never know.

How-To

  1. If you're not already keeping track of hire dates somewhere in Rock, create a new person attribute for the Staff Hire Date
    • Name: Staff Hire Date
    • Key: StaffHireDate
    • Required: No
    • Field Type: Date
  2. See if your HR department can backfill hire dates for existing staff members
  3. Add a new HTML block to the page that you'd like to display the celebration on. Ours is on the Internal Home Page.
  4. Add the following Lava to the content of the HTML block:
    {%- assign btnIconCSSClass = 'far fa-star' -%}
    {%- assign today = 'Now' | Date:'M/d' -%}
    {%- assign anniversaryDate = CurrentPerson.AnniversaryDate | AsDateTime -%}
    {%- assign birthDate = CurrentPerson.BirthDate | AsDateTime -%}
    {%- assign workiversaryDate = CurrentPerson | Attribute:'StaffHireDate' | AsDateTime -%}
    {%- assign anniversaryDay = anniversaryDate | Date:'M/d' -%}
    {%- assign birthDay = birthDate | Date:'M/d' -%}
    {%- assign workiversaryDay = workiversaryDate | Date:'M/d' -%}
    {%- assign isAnniversary = false -%}
    {%- assign isBirthday = false -%}
    {%- assign isWorkiversary = false -%}
    {%- if anniversaryDay == today %}{% assign isAnniversary = true %}{% endif -%}
    {%- if birthDay == today %}{% assign isBirthday = true %}{% endif -%}
    {%- if workiversaryDay == today %}{% assign isWorkiversary = true %}{% endif -%}
    {%- if isAnniversary == true or isBirthday == true or isWorkiversary == true -%}
        {%- assign thisYear = 'Now' | Date:'yyyy' | AsInteger -%}
        {%- assign anniversaryYear = anniversaryDate | Date:'yyyy' | Default:'0' | AsInteger -%}
        {%- assign workiversaryYear = workiversaryDate | Date:'yyyy' | Default:'0' | AsInteger -%}
        {%- assign anniversaryNum = 0 -%}
        {%- assign workiversaryNum = 0 -%}
        {%- if isAnniversary == true and isBirthday == true and isWorkiversary == true -%}
            {%- assign theme = 31 -%}
            {%- assign anniversaryNum = thisYear | Minus:anniversaryYear %}
            {%- assign workiversaryNum = thisYear | Minus:workiversaryYear %}
            {%- capture occasion %}Birthday, {{ anniversaryNum | AsString | NumberToOrdinal }} Anniversary and {{ workiversaryNum | AsString | NumberToOrdinal }} Workiversary{% endcapture -%}
        {%- elseif isBirthday == true and isAnniversary == true -%}
            {%- assign theme = 28 -%}
            {%- assign anniversaryNum = thisYear | Minus:anniversaryYear %}
            {%- capture occasion %}Birthday and {{ anniversaryNum | AsString | NumberToOrdinal }} Anniversary{% endcapture -%}
        {%- elseif isBirthday == true and isWorkiversary == true -%}
            {%- assign theme = 29 -%}
            {%- assign workiversaryNum = thisYear | Minus:workiversaryYear %}
            {%- capture occasion %}Birthday and {{ workiversaryNum | AsString | NumberToOrdinal }} Workiversary{% endcapture -%}
        {%- elseif isAnniversary == true and isWorkiversary == true -%}
            {%- assign theme = 30 -%}
            {%- assign anniversaryNum = thisYear | Minus:anniversaryYear %}
            {%- assign workiversaryNum = thisYear | Minus:workiversaryYear %}
            {%- capture occasion %}{{ anniversaryNum | AsString | NumberToOrdinal }} Anniversary and {{ workiversaryNum | AsString | NumberToOrdinal }} Workiversary{% endcapture -%}
        {%- elseif isAnniversary == true -%}
            {%- assign theme = 19 -%}
            {%- assign anniversaryNum = thisYear | Minus:anniversaryYear %}
            {%- capture occasion %}{{ anniversaryNum | AsString | NumberToOrdinal }} Anniversary{% endcapture -%}
        {%- elseif isWorkiversary == true -%}
            {%- assign theme = 7 -%}
            {%- assign workiversaryNum = thisYear | Minus:workiversaryYear %}
            {%- capture occasion %}{{ workiversaryNum | AsString | NumberToOrdinal }} Workiversary{% endcapture -%}
        {%- else -%}
            {%- assign theme = 1 -%}
            {%- assign occasion = 'Birthday' -%}
        {%- endif -%}
        {%- stylesheet compile:'less' -%}
            .btn-celebrate
            {
                margin: 0 auto 35px auto;
                padding: 10px 30px;
                border-radius: 40px;
                border: 5px solid #fff;
                font-size: 24px;
                font-weight: bold;
                white-space: normal;
                color: #fff;
                background: #f7921a;
                box-shadow: 0 4px 10px rgba(0,0,0,.1);
                transition: transform .25s cubic-bezier(0, 0, 0.35, 2.5);
                i:before
                {
                    display: inline-block;
                    transition: transform .4s cubic-bezier(.5,-2,.5,6);
                }
                &:hover, &:focus
                {
                    border-color: #fff;
                    color: #fff;
                    background: #de7e0b;
                    box-shadow: 0 2px 8px rgba(0,0,0,.15);
                }
                &:active, &:active:focus
                {
                    border-color: #fff;
                    color: #fff;
                    background: #e08b27;
                    box-shadow: 0 0 6px rgba(0,0,0,.15);
                }
                &:active
                {
                    transform: scale(.95);
                    transition: transform 50ms linear;
                    i:before
                    {
                        transform: rotate(10deg);
                        transition: transform 50ms linear;
                    }
                }
            }
        {%- endstylesheet -%}
    <div class="text-center"><a href="#" class="btn btn-default btn-celebrate" data-theme="{{ theme }}"><i class="{{ btnIconCSSClass }} fa-lg fa-flip-horizontal margin-r-sm"></i> Happy {{ occasion }}, {{ CurrentPerson.NickName }}! <i class="{{ btnIconCSSClass }} fa-lg margin-l-sm"></i></a></div>
        {%- javascript disableanonymousfunction:'true' -%}
            // Adapted from: https://codepen.io/Pillowfication/pen/PNEJbY
            // If set to true, the user must press the button to trigger
            // the confetti. Otherwise the confetti triggers on page load as well.
            var onlyOnClick = false;
    
            $(document).ready(function ()
            {
                // Globals
                var $window = $(window)
                    , random = Math.random
                    , cos = Math.cos
                    , sin = Math.sin
                    , PI = Math.PI
                    , PI2 = PI * 2
                    , timer = undefined
                    , frame = undefined
                    , confetti = [];
    
                var pointer = 0;
    
                var particles = 250
                    , spread = 40
                    , sizeMin = 5
                    , sizeMax = 15 - sizeMin
                    , eccentricity = 12
                    , deviation = 100
                    , dxThetaMin = -.1
                    , dxThetaMax = -dxThetaMin - dxThetaMin
                    , dyMin = .13
                    , dyMax = .18
                    , dThetaMin = .4
                    , dThetaMax = .7 - dThetaMin;
    
                var colorThemes =
                [
                    //multicolor
                    function () { return color(200 * random() | 0, 200 * random() | 0, 200 * random() | 0); }						//0 multicolor
                    , function () { var i = [2, 3, 4, 5, 6, 7, 8]; var p = 7 * random() | 0; return colorThemes[i[p]](); }			//1 alt multicolor (birthday)
    
                    //single colors
                    , function () { var b = 200 * random() | 0; return color(200, b, b); }											//2 reds
                    , function () { var p = random(); return color(240 - (40 * p | 0), 120 + (80 * p | 0), 200 * p | 0); }			//3 oranges
                    , function () { var p = random(); return color(240 - (40 * p | 0), 240 - (40 * p | 0), 200 * p | 0); }			//4 yellows
                    , function () { var b = 200 * random() | 0; return color(b, 200, b); }											//5 greens
                    , function () { return color(200 * random() | 0, 200, 200); }													//6 teals
                    , function () { var b = 150 * random() | 0; return color(b + 30, b + 50, 200); } 								//7 blues (workiversary)
                    , function () { var p = random(); return color(120 + (80 * p | 0), 40 + (140 * p | 0), 240 - (40 * p | 0)); }	//8 purples
                    , function () { var b = 256 * random() | 0; return color(b, b, b); }											//9 black/white
                    , function () { var g = 100 * random() | 0 + 100; return color(g, g, g); }										//10 grays
                    , function () { var w = 56 * random() | 0 + 200; return color(w, w, w); }										//11 white
    
                    //fun color combos
                    , function () { return mixThemes(2, 5); }						//12 red/green (Christmas)
                    , function () { return mixThemes(2, 7); }						//13 red/blue (July 4th)
                    , function () { return mixThemes(3, 8); }						//14 orange/purple (Halloween)
                    , function () { return mixThemes(3, 8, 9, 1 / 3, 2 / 3); }		//15 orange/purple/gray (Halloween)
                    , function () { return mixThemes(2, 11); }						//16 red/white (Valentines)
                    , function () { return mixThemes(7, 6); }						//17 blue/teal
                    , function () { return mixThemes(2, 6); }						//18 red/white/teal
                    , function () { return mixThemes(2, 4, 0, .8); }				//19 red/yellow (Anniversary)
                    , function () { return mixThemes(3, 4); }						//20 orange/yellow
                    , function () { return mixThemes(8, 4); }						//21 purple/yellow
                    , function () { return mixThemes(8, 20); }						//22 purple/orange/yellow
                    , function () { return mixThemes(8, 6); }						//23 purple/teal
                    , function () { return mixThemes(6, 7, 8, 1 / 3, 2 / 3); }		//24 blue/purple/teal
                    , function () { return mixThemes(2, 7, 8, 1 / 3, 2 / 3); }		//25 red/blue/purple
                    , function () { return mixThemes(2, 5, 7, 1 / 3, 2 / 3); }		//26 red/green/blue
                    , function () { var i = [2, 5, 7, 4, 8]; var p = 5 * random() | 0; return colorThemes[i[p]](); }	//27 red/green/blue/yellow/purple
    
                    //celebration combos
                    , function () { return mixThemes(1, 19); }						//28 birthday/anniversary
                    , function () { return mixThemes(1, 7); }						//29 birthday/workiversary
                    , function () { return mixThemes(19, 7); }						//30 anniversary/workiversary
                    , function () { return mixThemes(1, 19, 7, 1 / 3, 2 / 3); }		//31 birthday/anniversary/workiversary
                ];
    
                //color helper functions
                function color(r, g, b) { return 'rgb(' + r + ',' + g + ',' + b + ')'; }
                function mixThemes(c1, c2, c3 = 0, p1 = .5, p2 = 1.0)
                {
                    var r = random();
                    return colorThemes[r < p1 ? c1 : r < p2 ? c2 : c3]();
                }
                // Cosine interpolation
                function interpolation(a, b, t)
                {
                    return (1 - cos(PI * t)) / 2 * (b - a) + a;
                }
    
                // Create a 1D Maximal Poisson Disc over [0, 1]
                var radius = 1 / eccentricity, radius2 = radius + radius;
                function createPoisson() {
                    // domain is the set of points which are still available to pick from
                    // D = union{ [d_i, d_i+1] | i is even }
                    var domain = [radius, 1 - radius], measure = 1 - radius2, spline = [0, 1];
                    while (measure)
                    {
                        var dart = measure * random(), i, l, interval, a, b, c, d;
    
                        // Find where dart lies
                        for (i = 0, l = domain.length, measure = 0; i < l; i += 2)
                        {
                            a = domain[i], b = domain[i + 1], interval = b - a;
                            if (dart < measure + interval)
                            {
                                spline.push(dart += a - measure);
                                break;
                            }
                            measure += interval;
                        }
                        c = dart - radius, d = dart + radius;
    
                        // Update the domain
                        for (i = domain.length - 1; i > 0; i -= 2)
                        {
                            l = i - 1, a = domain[l], b = domain[i];
                            // c---d          c---d  Do nothing
                            //   c-----d  c-----d    Move interior
                            //   c--------------d    Delete interval
                            //         c--d          Split interval
                            //       a------b
                            if (a >= c && a < d)
                                if (b > d) domain[l] = d; // Move interior (Left case)
                                else domain.splice(l, 2); // Delete interval
                            else if (a < c && b > c)
                                if (b <= d) domain[i] = c; // Move interior (Right case)
                                else domain.splice(i, 0, c, d); // Split interval
                        }
    
                        // Re-measure the domain
                        for (i = 0, l = domain.length, measure = 0; i < l; i += 2)
                            measure += domain[i + 1] - domain[i];
                    }
                    return spline.sort();
                }
    
                // Create the overarching container
                var container = document.createElement('div');
                container.style.position = 'fixed';
                container.style.top = '0';
                container.style.left = '0';
                container.style.width = '100%';
                container.style.height = '0';
                container.style.overflow = 'visible';
                container.style.zIndex = '9999';
    
                // Confetto constructor
                function Confetto(theme)
                {
                    this.frame = 0;
                    this.outer = document.createElement('div');
                    this.inner = document.createElement('div');
                    this.outer.appendChild(this.inner);
    
                    var outerStyle = this.outer.style, innerStyle = this.inner.style;
                    outerStyle.position = 'absolute';
                    outerStyle.width = (sizeMin + sizeMax * random()) + 'px';
                    outerStyle.height = (sizeMin + sizeMax * random()) + 'px';
                    innerStyle.width = '100%';
                    innerStyle.height = '100%';
                    innerStyle.backgroundColor = theme();
    
                    outerStyle.perspective = '50px';
                    outerStyle.transform = 'rotate(' + (360 * random()) + 'deg)';
                    this.axis = 'rotate3D(' +
                        cos(360 * random()) + ',' +
                        cos(360 * random()) + ',0,';
                    this.theta = 360 * random();
                    this.dTheta = dThetaMin + dThetaMax * random();
                    innerStyle.transform = this.axis + this.theta + 'deg)';
    
                    this.x = $window.width() * random();
                    this.y = -deviation;
                    this.dx = sin(dxThetaMin + dxThetaMax * random());
                    this.dy = dyMin + dyMax * random();
                    outerStyle.left = this.x + 'px';
                    outerStyle.top = this.y + 'px';
    
                    // Create the periodic spline
                    this.splineX = createPoisson();
                    this.splineY = [];
                    for (var i = 1, l = this.splineX.length - 1; i < l; ++i)
                        this.splineY[i] = deviation * random();
                    this.splineY[0] = this.splineY[l] = deviation * random();
    
                    this.update = function (height, delta)
                    {
                        this.frame += delta;
                        this.x += this.dx * delta;
                        this.y += this.dy * delta;
                        this.theta += this.dTheta * delta;
    
                        // Compute spline and convert to polar
                        var phi = this.frame % 7777 / 7777, i = 0, j = 1;
                        while (phi >= this.splineX[j]) i = j++;
                        var rho = interpolation(
                            this.splineY[i],
                            this.splineY[j],
                            (phi - this.splineX[i]) / (this.splineX[j] - this.splineX[i])
                        );
                        phi *= PI2;
    
                        outerStyle.left = this.x + rho * cos(phi) + 'px';
                        outerStyle.top = this.y + rho * sin(phi) + 'px';
                        innerStyle.transform = this.axis + this.theta + 'deg)';
                        return this.y > height + deviation;
                    };
                }
                function poof()
                {
                    if (!frame)
                    {
                        // Append the container
                        document.body.appendChild(container);
    
                        // Add confetti
                        var theme = colorThemes[0],
                            count = 0;
    
                        if ($('a.btn-celebrate').data('theme') != '')
                        {
                            theme = colorThemes[$('a.btn-celebrate').data('theme')];
                        }
    
                        (function addConfetto()
                        {
                            if (++count > particles)
                                return timer = undefined;
    
                            var confetto = new Confetto(theme);
                            confetti.push(confetto);
                            container.appendChild(confetto.outer);
                            timer = setTimeout(addConfetto, spread * random());
                        })(0);
    
                        // Start the loop
                        var prev = undefined;
                        requestAnimationFrame(function loop(timestamp)
                        {
                            var delta = prev ? timestamp - prev : 0;
                            prev = timestamp;
                            var height = $window.height();
    
                            for (var i = confetti.length - 1; i >= 0; --i)
                            {
                                if (confetti[i].update(height, delta))
                                {
                                    container.removeChild(confetti[i].outer);
                                    confetti.splice(i, 1);
                                }
                            }
    
                            if (timer || confetti.length)
                                return frame = requestAnimationFrame(loop);
    
                            // Cleanup
                            document.body.removeChild(container);
                            frame = undefined;
                        });
                    }
                }
    
                // trigger confetti on page load
                if (!onlyOnClick) poof();
    
                // trigger confetti on button click
                $('a.btn-celebrate').on('click', function (e)
                {
                    e.preventDefault();
                    poof();
                    return false;
                })
            });
        {%- endjavascript -%}
    {%- endif -%}
            
  5. Wait for your next birthday, anniversary or workiversary and enjoy the show!

Bonus

Font Awesome doesn't currently include the "party popper" emoji, so I created one myself and added it to our own custom icon font. You can see an example of the icon in the screenshot below.

I attached the .svg file for my party popper icon to this recipe for anyone who's interested and knows how to add their own custom font to Rock. Use the Download File button to get it and feel free to use or modify the icon however you want.

By default, the recipe uses the star icon instead:


Please leave a comment or hit me up on Rock Chat if you have questions or find any issues with this recipe. Thanks!